From e5f3965ed118b64668e1f5503baf62b71a0fa863 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:27:49 -0700 Subject: [PATCH 01/16] feat(mship): add parallel subagents, improve streaming performance (#5122) * feat(subagents): add support for parallel subagents * fix(subagents): address parallel-subagent bugs * progress on streaming refactor * improvement(subagents): update comment to reflect new go feature flag * debug mode progress * remove debug logs * fix(validation): add escape annotation * improvement(code): remove dead fallbacks * fix subagent lane fallback issue * fix(mothership): increase default redis event limit to 100k from 5k * fix(mothership): streaming invariant projection enforcement --------- Co-authored-by: Vikhyath Mondreti --- .gitignore | 8 + apps/docs/components/icons.tsx | 18 + apps/docs/components/ui/icon-mapping.ts | 2 + .../content/docs/en/integrations/meta.json | 1 + .../docs/en/integrations/sportmonks.mdx | 1517 +++++++++++++++++ .../agent-group/agent-group.test.ts | 71 + .../components/agent-group/agent-group.tsx | 39 +- .../components/agent-group/index.ts | 2 +- .../components/agent-group/tool-call-item.tsx | 9 +- .../components/chat-content/chat-content.tsx | 9 + .../message-content/components/index.ts | 2 +- .../home/components/message-content/index.ts | 1 + .../message-content/message-content.test.ts | 67 + .../message-content/message-content.tsx | 127 +- .../components/message-content/utils.test.ts | 38 + .../home/components/message-content/utils.ts | 41 + .../mothership-chat/mothership-chat.tsx | 43 +- .../stream/dispatch-stream-event.test.ts | 16 +- .../hooks/stream/dispatch-stream-event.ts | 57 +- .../hooks/stream/handle-complete-event.ts | 27 +- .../home/hooks/stream/handle-error-event.ts | 23 +- .../home/hooks/stream/handle-run-event.ts | 56 +- .../home/hooks/stream/handle-span-event.ts | 89 +- .../home/hooks/stream/handle-text-event.ts | 54 +- .../hooks/stream/handle-tool-event.test.ts | 153 +- .../home/hooks/stream/handle-tool-event.ts | 255 +-- .../[workspaceId]/home/hooks/stream/index.ts | 1 + .../home/hooks/stream/stream-context.test.ts | 97 +- .../home/hooks/stream/stream-context.ts | 252 +-- .../home/hooks/stream/stream-helpers.ts | 89 +- .../hooks/stream/turn-model-serialize.test.ts | 408 +++++ .../home/hooks/stream/turn-model-serialize.ts | 337 ++++ .../home/hooks/stream/turn-model.test.ts | 486 ++++++ .../home/hooks/stream/turn-model.ts | 656 +++++++ .../[workspaceId]/home/hooks/use-chat.ts | 38 +- apps/sim/blocks/blocks/sportmonks.ts | 818 +++++++++ apps/sim/blocks/registry.ts | 3 + apps/sim/components/icons.tsx | 18 + apps/sim/lib/copilot/chat/display-message.ts | 43 +- .../lib/copilot/chat/effective-transcript.ts | 21 +- .../generated/mothership-stream-v1-schema.ts | 3 + .../copilot/generated/mothership-stream-v1.ts | 1 + .../request/context/request-context.ts | 6 +- .../copilot/request/context/result.test.ts | 3 +- .../request/go/file-preview-adapter.ts | 155 +- .../sim/lib/copilot/request/go/stream.test.ts | 6 +- apps/sim/lib/copilot/request/go/stream.ts | 33 +- .../copilot/request/handlers/handlers.test.ts | 66 +- .../sim/lib/copilot/request/handlers/index.ts | 17 +- apps/sim/lib/copilot/request/handlers/run.ts | 3 + apps/sim/lib/copilot/request/handlers/span.ts | 12 +- apps/sim/lib/copilot/request/handlers/text.ts | 23 +- apps/sim/lib/copilot/request/handlers/tool.ts | 15 +- .../sim/lib/copilot/request/handlers/types.ts | 40 +- .../lifecycle/resume-leg-context.test.ts | 82 + apps/sim/lib/copilot/request/lifecycle/run.ts | 333 +++- .../copilot/request/session/buffer.test.ts | 2 +- .../sim/lib/copilot/request/session/buffer.ts | 2 +- .../sim/lib/copilot/request/tools/executor.ts | 8 +- apps/sim/lib/copilot/request/types.ts | 81 +- apps/sim/lib/copilot/tool-executor/types.ts | 7 + .../tools/registry/server-tool-adapter.ts | 1 + .../sim/lib/copilot/tools/server/base-tool.ts | 7 + .../tools/server/files/edit-content.ts | 6 + .../server/files/file-intent-store.test.ts | 122 ++ .../tools/server/files/file-intent-store.ts | 25 +- .../tools/server/files/workspace-file.ts | 3 + apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 221 ++- apps/sim/tools/registry.ts | 106 ++ apps/sim/tools/sportmonks/types.ts | 89 + apps/sim/tools/sportmonks_core/get_cities.ts | 104 ++ apps/sim/tools/sportmonks_core/get_city.ts | 83 + .../tools/sportmonks_core/get_continent.ts | 83 + .../tools/sportmonks_core/get_continents.ts | 104 ++ .../tools/sportmonks_core/get_countries.ts | 104 ++ apps/sim/tools/sportmonks_core/get_country.ts | 83 + apps/sim/tools/sportmonks_core/get_region.ts | 83 + apps/sim/tools/sportmonks_core/get_regions.ts | 104 ++ .../tools/sportmonks_core/get_timezones.ts | 61 + apps/sim/tools/sportmonks_core/get_type.ts | 74 + apps/sim/tools/sportmonks_core/get_types.ts | 93 + apps/sim/tools/sportmonks_core/index.ts | 13 + .../tools/sportmonks_core/search_cities.ts | 103 ++ .../tools/sportmonks_core/search_countries.ts | 103 ++ apps/sim/tools/sportmonks_core/types.ts | 172 ++ .../tools/sportmonks_football/get_fixture.ts | 90 + .../get_fixtures_by_date.ts | 116 ++ .../get_fixtures_by_date_range.ts | 126 ++ .../sportmonks_football/get_head_to_head.ts | 125 ++ .../get_inplay_livescores.ts | 80 + .../tools/sportmonks_football/get_league.ts | 90 + .../tools/sportmonks_football/get_leagues.ts | 105 ++ .../sportmonks_football/get_livescores.ts | 80 + .../tools/sportmonks_football/get_player.ts | 90 + .../get_standings_by_season.ts | 90 + .../sim/tools/sportmonks_football/get_team.ts | 88 + .../sportmonks_football/get_team_squad.ts | 89 + .../get_topscorers_by_season.ts | 117 ++ apps/sim/tools/sportmonks_football/index.ts | 15 + .../sportmonks_football/search_players.ts | 116 ++ .../tools/sportmonks_football/search_teams.ts | 115 ++ apps/sim/tools/sportmonks_football/types.ts | 392 +++++ .../tools/sportmonks_motorsport/get_driver.ts | 89 + .../get_driver_standings_by_season.ts | 116 ++ .../sportmonks_motorsport/get_drivers.ts | 104 ++ .../sportmonks_motorsport/get_fixture.ts | 90 + .../get_fixtures_by_date.ts | 116 ++ .../get_laps_by_fixture.ts | 90 + .../sportmonks_motorsport/get_livescores.ts | 105 ++ .../get_pitstops_by_fixture.ts | 91 + .../tools/sportmonks_motorsport/get_team.ts | 89 + .../get_team_standings_by_season.ts | 116 ++ .../tools/sportmonks_motorsport/get_teams.ts | 104 ++ apps/sim/tools/sportmonks_motorsport/index.ts | 12 + .../sportmonks_motorsport/search_drivers.ts | 109 ++ apps/sim/tools/sportmonks_motorsport/types.ts | 317 ++++ .../tools/sportmonks_odds/get_bookmaker.ts | 74 + .../tools/sportmonks_odds/get_bookmakers.ts | 98 ++ .../get_inplay_odds_by_fixture.ts | 116 ++ apps/sim/tools/sportmonks_odds/get_market.ts | 74 + apps/sim/tools/sportmonks_odds/get_markets.ts | 98 ++ .../get_pre_match_odds_by_fixture.ts | 115 ++ apps/sim/tools/sportmonks_odds/index.ts | 8 + .../sportmonks_odds/search_bookmakers.ts | 97 ++ .../tools/sportmonks_odds/search_markets.ts | 97 ++ apps/sim/tools/sportmonks_odds/types.ts | 230 +++ 127 files changed, 12197 insertions(+), 1121 deletions(-) create mode 100644 apps/docs/content/docs/en/integrations/sportmonks.mdx create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts create mode 100644 apps/sim/blocks/blocks/sportmonks.ts create mode 100644 apps/sim/lib/copilot/request/lifecycle/resume-leg-context.test.ts create mode 100644 apps/sim/lib/copilot/tools/server/files/file-intent-store.test.ts create mode 100644 apps/sim/tools/sportmonks/types.ts create mode 100644 apps/sim/tools/sportmonks_core/get_cities.ts create mode 100644 apps/sim/tools/sportmonks_core/get_city.ts create mode 100644 apps/sim/tools/sportmonks_core/get_continent.ts create mode 100644 apps/sim/tools/sportmonks_core/get_continents.ts create mode 100644 apps/sim/tools/sportmonks_core/get_countries.ts create mode 100644 apps/sim/tools/sportmonks_core/get_country.ts create mode 100644 apps/sim/tools/sportmonks_core/get_region.ts create mode 100644 apps/sim/tools/sportmonks_core/get_regions.ts create mode 100644 apps/sim/tools/sportmonks_core/get_timezones.ts create mode 100644 apps/sim/tools/sportmonks_core/get_type.ts create mode 100644 apps/sim/tools/sportmonks_core/get_types.ts create mode 100644 apps/sim/tools/sportmonks_core/index.ts create mode 100644 apps/sim/tools/sportmonks_core/search_cities.ts create mode 100644 apps/sim/tools/sportmonks_core/search_countries.ts create mode 100644 apps/sim/tools/sportmonks_core/types.ts create mode 100644 apps/sim/tools/sportmonks_football/get_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_fixtures_by_date.ts create mode 100644 apps/sim/tools/sportmonks_football/get_fixtures_by_date_range.ts create mode 100644 apps/sim/tools/sportmonks_football/get_head_to_head.ts create mode 100644 apps/sim/tools/sportmonks_football/get_inplay_livescores.ts create mode 100644 apps/sim/tools/sportmonks_football/get_league.ts create mode 100644 apps/sim/tools/sportmonks_football/get_leagues.ts create mode 100644 apps/sim/tools/sportmonks_football/get_livescores.ts create mode 100644 apps/sim/tools/sportmonks_football/get_player.ts create mode 100644 apps/sim/tools/sportmonks_football/get_standings_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_team_squad.ts create mode 100644 apps/sim/tools/sportmonks_football/get_topscorers_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/index.ts create mode 100644 apps/sim/tools/sportmonks_football/search_players.ts create mode 100644 apps/sim/tools/sportmonks_football/search_teams.ts create mode 100644 apps/sim/tools/sportmonks_football/types.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_driver.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_driver_standings_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_drivers.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_livescores.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_team.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_team_standings_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_teams.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/index.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/search_drivers.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/types.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_bookmaker.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_bookmakers.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_market.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_markets.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_odds/index.ts create mode 100644 apps/sim/tools/sportmonks_odds/search_bookmakers.ts create mode 100644 apps/sim/tools/sportmonks_odds/search_markets.ts create mode 100644 apps/sim/tools/sportmonks_odds/types.ts diff --git a/.gitignore b/.gitignore index c38b288a683..a700a66602a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ # bun specific bun-debug.log* +# cursor debug logs +.cursor/debug-*.log + # this repo uses bun.lock; package-lock.json files are accidental package-lock.json @@ -44,6 +47,11 @@ dump.rdb .env.test .env.production +# editor swap files +*.swp +*.swo +*.swn + # vercel .vercel diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 13fa62588bf..9792fb9b7c2 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1958,6 +1958,24 @@ export function WhatsAppIcon(props: SVGProps) { ) } +export function SportmonksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function SquareIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 22cf6c737db..05866ee2fd6 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -195,6 +195,7 @@ import { SixtyfourIcon, SlackIcon, SmtpIcon, + SportmonksIcon, SQSIcon, SquareIcon, SshIcon, @@ -449,6 +450,7 @@ export const blockTypeToIconMap: Record = { sixtyfour: SixtyfourIcon, slack: SlackIcon, smtp: SmtpIcon, + sportmonks: SportmonksIcon, sqs: SQSIcon, square: SquareIcon, ssh: SshIcon, diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 5e08cf704b0..de492727e78 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -196,6 +196,7 @@ "sixtyfour", "slack", "smtp", + "sportmonks", "sqs", "square", "ssh", diff --git a/apps/docs/content/docs/en/integrations/sportmonks.mdx b/apps/docs/content/docs/en/integrations/sportmonks.mdx new file mode 100644 index 00000000000..21383de4d6f --- /dev/null +++ b/apps/docs/content/docs/en/integrations/sportmonks.mdx @@ -0,0 +1,1517 @@ +--- +title: Sportmonks +description: Access Sportmonks football, motorsport, odds, and reference data +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, teams, squads, players, standings, and topscorers. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones. + + + +## Actions + +### `sportmonks_football_get_livescores` + +Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of live fixture objects | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_inplay_livescores` + +Retrieve all fixtures that are currently being played (in-play) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of in-play fixture objects | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixtures_by_date` + +Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;league\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects for the requested date | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixtures_by_date_range` + +Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max range is 100 days. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | Start date in YYYY-MM-DD format | +| `endDate` | string | Yes | End date in YYYY-MM-DD format \(max 100 days after start\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects within the requested date range | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixture` + +Retrieve a single football fixture by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events;lineups;statistics\) | +| `filters` | string | No | Filters to apply \(e.g. eventTypes:14\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixture` | object | The requested fixture object | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_head_to_head` + +Retrieve the head-to-head fixtures between two teams from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `team1` | string | Yes | The id of the first team | +| `team2` | string | Yes | The id of the second team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of head-to-head fixture objects between the two teams | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_leagues` + +Retrieve all football leagues available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_league` + +Retrieve a single football league by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason;seasons\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `league` | object | The requested league object | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_search_teams` + +Search for football teams by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The team name to search for \(e.g. Celtic\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order teams by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team objects matching the search query | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | + +### `sportmonks_football_get_team` + +Retrieve a single football team by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue;coaches;players.player\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `team` | object | The requested team object | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | + +### `sportmonks_football_get_team_squad` + +Retrieve the current domestic squad for a team by team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `squad` | array | Array of squad entries for the team | +| ↳ `id` | number | Unique id of the squad record | +| ↳ `transfer_id` | number | Transfer id of the squad record | +| ↳ `player_id` | number | Player in the squad | +| ↳ `team_id` | number | Team of the squad | +| ↳ `position_id` | number | Position of the player in the squad | +| ↳ `detailed_position_id` | number | Detailed position of the player in the squad | +| ↳ `jersey_number` | number | Jersey number of the player | +| ↳ `start` | string | Start contract date of the player | +| ↳ `end` | string | End contract date of the player | + +### `sportmonks_football_search_players` + +Search for football players by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The player name to search for \(e.g. Tavernier\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order players by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `players` | array | Array of player objects matching the search query | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | + +### `sportmonks_football_get_player` + +Retrieve a single football player by their ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `playerId` | string | Yes | The unique id of the player | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team;statistics\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `player` | object | The requested player object | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | + +### `sportmonks_football_get_standings_by_season` + +Retrieve the full league standings table for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details;form\) | +| `filters` | string | No | Filters to apply \(e.g. standingStages:77453568\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of standing entries for the season | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Group related to the standing | +| ↳ `round_id` | number | Round related to the standing | +| ↳ `standing_rule_id` | number | Standing rule related to the standing | +| ↳ `position` | number | Position of the team in the standing | +| ↳ `result` | string | Movement of the team in the standing | +| ↳ `points` | number | Points the team has gathered | + +### `sportmonks_football_get_topscorers_by_season` + +Retrieve the topscorers (goals, assists, cards) for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;participant;type\) | +| `filters` | string | No | Filters to apply \(e.g. seasontopscorerTypes:208\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order topscorers by position \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `topscorers` | array | Array of topscorer entries for the season | +| ↳ `id` | number | Unique id of the topscorer record | +| ↳ `season_id` | number | Season related to the topscorer | +| ↳ `league_id` | number | League related to the topscorer | +| ↳ `stage_id` | number | Stage related to the topscorer | +| ↳ `player_id` | number | Player related to the topscorer | +| ↳ `participant_id` | number | Team related to the topscorer | +| ↳ `type_id` | number | Type of the topscorer \(goals, assists, cards\) | +| ↳ `position` | number | Position of the topscorer | +| ↳ `total` | number | Number of goals, assists or cards | + +### `sportmonks_motorsport_get_livescores` + +Retrieve all live motorsport fixtures (sessions) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of live motorsport fixture \(session\) objects | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_fixtures_by_date` + +Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of motorsport fixture \(session\) objects for the requested date | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_fixture` + +Retrieve a single motorsport fixture (session) by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results;latestLaps;pitstops\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixture` | object | The requested motorsport fixture \(session\) object | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_drivers` + +Retrieve all motorsport drivers from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of driver objects | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_driver` + +Retrieve a single motorsport driver by their ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `driverId` | string | Yes | The unique id of the driver | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `driver` | object | The requested driver object | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_search_drivers` + +Search for motorsport drivers by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The driver name to search for \(e.g. Verstappen\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of driver objects matching the search query | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_teams` + +Retrieve all motorsport teams (constructors) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team \(constructor\) objects | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_get_team` + +Retrieve a single motorsport team (constructor) by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team \(constructor\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `team` | object | The requested team \(constructor\) object | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_get_driver_standings_by_season` + +Retrieve the drivers championship standings for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of driver standing entries for the season | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Driver or team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `standing_rule_id` | number | Not used in the Motorsport API | +| ↳ `position` | number | Position of the participant in the standing | +| ↳ `result` | string | Not used in the Motorsport API | +| ↳ `points` | number | Points the participant has gathered | + +### `sportmonks_motorsport_get_team_standings_by_season` + +Retrieve the constructors championship standings for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of team \(constructor\) standing entries for the season | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Driver or team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `standing_rule_id` | number | Not used in the Motorsport API | +| ↳ `position` | number | Position of the participant in the standing | +| ↳ `result` | string | Not used in the Motorsport API | +| ↳ `points` | number | Points the participant has gathered | + +### `sportmonks_motorsport_get_laps_by_fixture` + +Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `laps` | array | Array of lap objects for the fixture | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_pitstops_by_fixture` + +Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pitstops` | array | Array of pitstop objects for the fixture | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_odds_get_pre_match_odds_by_fixture` + +Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of pre-match odd objects for the fixture | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | +| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_inplay_odds_by_fixture` + +Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of in-play odd objects for the fixture | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `external_id` | number | External id of the odd | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `suspended` | boolean | Whether the odd is suspended | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_bookmakers` + +Retrieve all bookmakers from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `filters` | string | No | Filters to apply \(e.g. IdAfter:bookmakerID\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmakers` | array | Array of bookmaker objects | +| ↳ `id` | number | Unique id of the bookmaker | +| ↳ `name` | string | Name of the bookmaker | +| ↳ `logo` | string | Logo of the bookmaker | + +### `sportmonks_odds_get_bookmaker` + +Retrieve a single bookmaker by its ID from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `bookmakerId` | string | Yes | The unique id of the bookmaker | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmaker` | object | The requested bookmaker object | +| ↳ `id` | number | Unique id of the bookmaker | +| ↳ `name` | string | Name of the bookmaker | +| ↳ `logo` | string | Logo of the bookmaker | + +### `sportmonks_odds_search_bookmakers` + +Search for bookmakers by name from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The bookmaker name to search for \(e.g. bet365\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmakers` | array | Array of bookmaker objects matching the search query | +| ↳ `id` | number | Unique id of the bookmaker | +| ↳ `name` | string | Name of the bookmaker | +| ↳ `logo` | string | Logo of the bookmaker | + +### `sportmonks_odds_get_markets` + +Retrieve all betting markets from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `filters` | string | No | Filters to apply \(e.g. IdAfter:marketID\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `markets` | array | Array of market objects | +| ↳ `id` | number | Unique id of the market | +| ↳ `name` | string | Name of the market | + +### `sportmonks_odds_get_market` + +Retrieve a single betting market by its ID from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `marketId` | string | Yes | The unique id of the market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `market` | object | The requested market object | +| ↳ `id` | number | Unique id of the market | +| ↳ `name` | string | Name of the market | + +### `sportmonks_odds_search_markets` + +Search for betting markets by name from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The market name to search for \(e.g. Over/Under\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `markets` | array | Array of market objects matching the search query | +| ↳ `id` | number | Unique id of the market | +| ↳ `name` | string | Name of the market | + +### `sportmonks_core_get_continents` + +Retrieve all continents from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. countries\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `continents` | array | Array of continent objects | +| ↳ `id` | number | Unique id of the continent | +| ↳ `name` | string | Name of the continent | +| ↳ `code` | string | Short code of the continent | + +### `sportmonks_core_get_continent` + +Retrieve a single continent by its ID from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `continentId` | string | Yes | The unique id of the continent | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. countries\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `continent` | object | The requested continent object | +| ↳ `id` | number | Unique id of the continent | +| ↳ `name` | string | Name of the continent | +| ↳ `code` | string | Short code of the continent | + +### `sportmonks_core_get_countries` + +Retrieve all countries from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent;regions\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `countries` | array | Array of country objects | +| ↳ `id` | number | Unique id of the country | +| ↳ `continent_id` | number | Continent of the country | +| ↳ `name` | string | Name of the country | +| ↳ `official_name` | string | Official name of the country | +| ↳ `fifa_name` | string | Official FIFA short code name | +| ↳ `iso2` | string | Two letter country code | +| ↳ `iso3` | string | Three letter country code | +| ↳ `latitude` | string | Latitude position of the country | +| ↳ `longitude` | string | Longitude position of the country | +| ↳ `geonameid` | number | Official geonameid | +| ↳ `borders` | array | Neighbouring countries \(ISO3 codes\) | +| ↳ `image_path` | string | Image path to the country flag | + +### `sportmonks_core_get_country` + +Retrieve a single country by its ID from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent;regions\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `country` | object | The requested country object | +| ↳ `id` | number | Unique id of the country | +| ↳ `continent_id` | number | Continent of the country | +| ↳ `name` | string | Name of the country | +| ↳ `official_name` | string | Official name of the country | +| ↳ `fifa_name` | string | Official FIFA short code name | +| ↳ `iso2` | string | Two letter country code | +| ↳ `iso3` | string | Three letter country code | +| ↳ `latitude` | string | Latitude position of the country | +| ↳ `longitude` | string | Longitude position of the country | +| ↳ `geonameid` | number | Official geonameid | +| ↳ `borders` | array | Neighbouring countries \(ISO3 codes\) | +| ↳ `image_path` | string | Image path to the country flag | + +### `sportmonks_core_search_countries` + +Search for countries by name from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The country name to search for \(e.g. Brazil\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `countries` | array | Array of country objects matching the search query | +| ↳ `id` | number | Unique id of the country | +| ↳ `continent_id` | number | Continent of the country | +| ↳ `name` | string | Name of the country | +| ↳ `official_name` | string | Official name of the country | +| ↳ `fifa_name` | string | Official FIFA short code name | +| ↳ `iso2` | string | Two letter country code | +| ↳ `iso3` | string | Three letter country code | +| ↳ `latitude` | string | Latitude position of the country | +| ↳ `longitude` | string | Longitude position of the country | +| ↳ `geonameid` | number | Official geonameid | +| ↳ `borders` | array | Neighbouring countries \(ISO3 codes\) | +| ↳ `image_path` | string | Image path to the country flag | + +### `sportmonks_core_get_regions` + +Retrieve all regions from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `regions` | array | Array of region objects | +| ↳ `id` | number | Unique id of the region | +| ↳ `country_id` | number | Country of the region | +| ↳ `name` | string | Name of the region | + +### `sportmonks_core_get_region` + +Retrieve a single region by its ID from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `regionId` | string | Yes | The unique id of the region | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `region` | object | The requested region object | +| ↳ `id` | number | Unique id of the region | +| ↳ `country_id` | number | Country of the region | +| ↳ `name` | string | Name of the region | + +### `sportmonks_core_get_cities` + +Retrieve all cities from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cities` | array | Array of city objects | +| ↳ `id` | number | Unique id of the city | +| ↳ `country_id` | number | Country of the city | +| ↳ `region` | number | Region of the city | +| ↳ `name` | string | Name of the city | +| ↳ `latitude` | string | Latitude of the city | +| ↳ `longitude` | string | Longitude of the city | +| ↳ `geonameid` | number | Official geonameid of the city | + +### `sportmonks_core_get_city` + +Retrieve a single city by its ID from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `cityId` | string | Yes | The unique id of the city | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `city` | object | The requested city object | +| ↳ `id` | number | Unique id of the city | +| ↳ `country_id` | number | Country of the city | +| ↳ `region` | number | Region of the city | +| ↳ `name` | string | Name of the city | +| ↳ `latitude` | string | Latitude of the city | +| ↳ `longitude` | string | Longitude of the city | +| ↳ `geonameid` | number | Official geonameid of the city | + +### `sportmonks_core_search_cities` + +Search for cities by name from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The city name to search for \(e.g. London\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cities` | array | Array of city objects matching the search query | +| ↳ `id` | number | Unique id of the city | +| ↳ `country_id` | number | Country of the city | +| ↳ `region` | number | Region of the city | +| ↳ `name` | string | Name of the city | +| ↳ `latitude` | string | Latitude of the city | +| ↳ `longitude` | string | Longitude of the city | +| ↳ `geonameid` | number | Official geonameid of the city | + +### `sportmonks_core_get_types` + +Retrieve all types (reference data describing events, statistics, positions, etc.) from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `types` | array | Array of type objects | +| ↳ `id` | number | Unique id of the type | +| ↳ `parent_id` | number | Parent type of the type | +| ↳ `name` | string | Name of the type | +| ↳ `code` | string | Code of the type | +| ↳ `developer_name` | string | Developer name of the type | +| ↳ `group` | string | Group the type falls under | +| ↳ `description` | string | Description of the type | + +### `sportmonks_core_get_type` + +Retrieve a single type by its ID from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `typeId` | string | Yes | The unique id of the type | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | object | The requested type object | +| ↳ `id` | number | Unique id of the type | +| ↳ `parent_id` | number | Parent type of the type | +| ↳ `name` | string | Name of the type | +| ↳ `code` | string | Code of the type | +| ↳ `developer_name` | string | Developer name of the type | +| ↳ `group` | string | Group the type falls under | +| ↳ `description` | string | Description of the type | + +### `sportmonks_core_get_timezones` + +Retrieve all supported time zones (IANA names) from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timezones` | array | Array of supported IANA time zone names \(e.g. Europe/London\) | + + diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts new file mode 100644 index 00000000000..842034b9d75 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts @@ -0,0 +1,71 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { ToolCallData, ToolCallStatus } from '../../../../types' +import type { AgentGroupItem } from './agent-group' +import { isAgentGroupResolved } from './agent-group' + +let toolSeq = 0 + +function tool(status: ToolCallStatus): AgentGroupItem { + toolSeq += 1 + const data: ToolCallData = { + id: `tool-${toolSeq}`, + toolName: 'grep', + displayTitle: 'Searching', + status, + } + return { type: 'tool', data } +} + +function text(content: string): AgentGroupItem { + return { type: 'text', content } +} + +function group(items: AgentGroupItem[], isDelegating = false): AgentGroupItem { + return { + type: 'agent_group', + group: { + id: `group-${toolSeq}`, + agentName: 'deploy', + agentLabel: 'Deploy', + items, + isDelegating, + isOpen: true, + }, + } +} + +describe('isAgentGroupResolved', () => { + it('is unresolved when there is no work yet', () => { + expect(isAgentGroupResolved([])).toBe(false) + expect(isAgentGroupResolved([text('thinking...')])).toBe(false) + }) + + it('resolves once every own tool is terminal', () => { + expect(isAgentGroupResolved([tool('success')])).toBe(true) + expect(isAgentGroupResolved([tool('success'), tool('error')])).toBe(true) + }) + + it('stays unresolved while any own tool is still executing', () => { + expect(isAgentGroupResolved([tool('success'), tool('executing')])).toBe(false) + }) + + it('resolves a parent whose only work is a finished child group', () => { + expect(isAgentGroupResolved([group([tool('success')])])).toBe(true) + }) + + it('stays unresolved while a nested child is still delegating', () => { + expect(isAgentGroupResolved([group([], true)])).toBe(false) + }) + + it('stays unresolved while a nested child has an executing tool', () => { + expect(isAgentGroupResolved([group([tool('executing')])])).toBe(false) + }) + + it('resolves deep nesting only when every descendant is terminal', () => { + expect(isAgentGroupResolved([group([group([tool('success')])])])).toBe(true) + expect(isAgentGroupResolved([group([group([tool('executing')])])])).toBe(false) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index eb42f24729c..a88a39160b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -4,7 +4,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { ChevronDown, Expandable, ExpandableContent, PillsRing } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' -import { getAgentIcon } from '../../utils' +import { getAgentIcon, isToolDone } from '../../utils' import { ToolCallItem } from './tool-call-item' /** @@ -35,15 +35,18 @@ interface AgentGroupProps { defaultExpanded?: boolean } -function isToolDone(status: ToolCallData['status']): boolean { - return ( - status === 'success' || - status === 'error' || - status === 'cancelled' || - status === 'skipped' || - status === 'rejected' || - status === 'interrupted' - ) +export function isAgentGroupResolved(items: AgentGroupItem[]): boolean { + let hasWork = false + for (const item of items) { + if (item.type === 'tool') { + hasWork = true + if (!isToolDone(item.data.status)) return false + } else if (item.type === 'agent_group') { + hasWork = true + if (item.group.isDelegating || !isAgentGroupResolved(item.group.items)) return false + } + } + return hasWork } export function AgentGroup({ @@ -56,20 +59,18 @@ export function AgentGroup({ }: AgentGroupProps) { const AgentIcon = getAgentIcon(agentName) const hasItems = items.length > 0 - const toolItems = items.filter( - (item): item is Extract => item.type === 'tool' - ) - const allDone = toolItems.length > 0 && toolItems.every((t) => isToolDone(t.data.status)) - // Only a live turn can be delegating. Once the turn is terminal (complete, - // errored, or stopped) no subagent should spin — even one aborted before its - // first tool call, where `allDone` is false because there are no tools yet. - const showDelegatingSpinner = isStreaming && isDelegating && !allDone + const resolved = isAgentGroupResolved(items) + // Pure projection of the run's own state: a subagent header spins while it is + // delegating with no resolved work yet. A terminal turn closes the lane (its + // subagent block is stamped ended), which clears `isDelegating`, so no + // transport gating is needed to stop an aborted-before-first-tool spinner. + const showDelegatingSpinner = isDelegating && !resolved // Expand only while the turn is live and the group is still open or working. // Once the turn ends (isStreaming false) — or a subagent closes mid-turn — the // group auto-collapses, so finished subagent blocks never stay expanded. A // manual toggle pins the choice for the rest of the message. - const autoExpanded = isStreaming && (defaultExpanded || !allDone) + const autoExpanded = isStreaming && (defaultExpanded || !resolved) const [manualExpanded, setManualExpanded] = useState(null) const expanded = manualExpanded ?? autoExpanded diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/index.ts index 463df56fd03..812d818e9a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/index.ts @@ -1,3 +1,3 @@ export type { AgentGroupItem, NestedAgentGroup } from './agent-group' -export { AgentGroup } from './agent-group' +export { AgentGroup, isAgentGroupResolved } from './agent-group' export { CircleStop } from './tool-call-item' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx index 02fcccb5044..6b8baa463fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { PillsRing } from '@/components/emcn' import { WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' import type { ToolCallStatus } from '../../../../types' -import { getToolIcon } from '../../utils' +import { getToolIcon, resolveToolDisplayState } from '../../utils' function CircleCheck({ className }: { className?: string }) { return ( @@ -58,13 +58,14 @@ function Hyphen({ className }: { className?: string }) { } function StatusIcon({ status, toolName }: { status: ToolCallStatus; toolName: string }) { - if (status === 'executing') { + const display = resolveToolDisplayState(status) + if (display === 'spinner') { return } - if (status === 'cancelled') { + if (display === 'cancelled') { return } - if (status === 'interrupted') { + if (display === 'interrupted') { return } const Icon = getToolIcon(toolName) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index 39acbce7b6c..3075698e179 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -279,6 +279,7 @@ interface ChatContentProps { isStreaming?: boolean onOptionSelect?: (id: string) => void onWorkspaceResourceSelect?: (resource: MothershipResource) => void + onRevealStateChange?: (isRevealing: boolean) => void } function ChatContentInner({ @@ -286,14 +287,22 @@ function ChatContentInner({ isStreaming = false, onOptionSelect, onWorkspaceResourceSelect, + onRevealStateChange, }: ChatContentProps) { const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect) onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect + const onRevealStateChangeRef = useRef(onRevealStateChange) + onRevealStateChangeRef.current = onRevealStateChange + const displayContent = useMemo(() => sanitizeChatDisplayContent(content), [content]) const streamedContent = useSmoothText(displayContent, isStreaming) const isRevealing = isStreaming || streamedContent.length < displayContent.length + useEffect(() => { + onRevealStateChangeRef.current?.(isRevealing) + }, [isRevealing]) + /** * One-way latch: once a message has streamed in this mount, keep rendering it * through Streamdown's streaming/animation pipeline for the rest of its life. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts index 8d1105840a6..4a9a1a2ddf0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts @@ -1,5 +1,5 @@ export type { AgentGroupItem, NestedAgentGroup } from './agent-group' -export { AgentGroup, CircleStop } from './agent-group' +export { AgentGroup, CircleStop, isAgentGroupResolved } from './agent-group' export { ChatContent } from './chat-content' export { Options } from './options' export { PendingTagIndicator, parseSpecialTags, SpecialTags } from './special-tags' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/index.ts index 53b2b63195b..1d1a257d880 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/index.ts @@ -2,3 +2,4 @@ export { assistantMessageHasRenderableContent, MessageContent, } from './message-content' +export type { MessagePhase } from './utils' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts index 02a9f247674..b6a93bfbb34 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts @@ -56,6 +56,23 @@ describe('parseBlocks span-identity tree', () => { expect(nested.group.items.some((item) => item.type === 'tool')).toBe(true) }) + it('clears the parent delegating flag once it has spawned a child, leaving only the child active', () => { + const blocks: ContentBlock[] = [ + subagentStart('workflow', 'S1', 'main'), + subagentStart('deploy', 'S2', 'S1'), + ] + + const segments = parseBlocks(blocks) + expect(segments).toHaveLength(1) + const workflow = segments[0] + if (workflow.type !== 'agent_group') throw new Error('expected workflow group') + expect(workflow.isDelegating).toBe(false) + + const nested = workflow.items.find((item) => item.type === 'agent_group') + if (!nested || nested.type !== 'agent_group') throw new Error('expected nested deploy group') + expect(nested.group.isDelegating).toBe(true) + }) + it('keeps two top-level subagents as siblings', () => { const blocks: ContentBlock[] = [ subagentStart('workflow', 'S1', 'main'), @@ -94,6 +111,56 @@ describe('parseBlocks span-identity tree', () => { expect(withContent[0].isDelegating).toBe(false) }) + it('keeps two concurrently-open subagent lanes separate with interleaved text', () => { + const blocks: ContentBlock[] = [ + subagentStart('research', 'A', 'main'), + subagentStart('research', 'B', 'main'), + { type: 'subagent_text', content: 'A1 ', spanId: 'A', subagent: 'research', timestamp: 2 }, + { type: 'subagent_text', content: 'B1 ', spanId: 'B', subagent: 'research', timestamp: 2 }, + { type: 'subagent_text', content: 'A2', spanId: 'A', subagent: 'research', timestamp: 3 }, + ] + + const segments = parseBlocks(blocks) + const groups = segments.filter((s) => s.type === 'agent_group') + expect(groups).toHaveLength(2) + + const textOf = (g: (typeof groups)[number]): string => { + if (g.type !== 'agent_group') return '' + return g.items + .filter((i) => i.type === 'text') + .map((i) => (i.type === 'text' ? i.content : '')) + .join('') + } + // Group A (spanId A) created first, group B second. Interleaved chunks stay + // in their own lane and in order — no cross-contamination. + expect(textOf(groups[0])).toBe('A1 A2') + expect(textOf(groups[1])).toBe('B1 ') + }) + + it('renders a persisted subagent lane as closed when only endedAt is set (no subagent_end)', () => { + // The Sim backend stamps endedAt on the subagent block but does not emit a + // separate subagent_end block; a reloaded transcript must still show the + // lane closed (no stuck delegating spinner). + const blocks: ContentBlock[] = [ + { + type: 'subagent', + content: 'research', + spanId: 'S1', + parentSpanId: 'main', + timestamp: 1, + endedAt: 5, + }, + { type: 'subagent_text', content: 'done', spanId: 'S1', subagent: 'research', timestamp: 2 }, + ] + + const segments = parseBlocks(blocks) + const group = segments.find((s) => s.type === 'agent_group') + expect(group).toBeDefined() + if (!group || group.type !== 'agent_group') throw new Error('expected research group') + expect(group.isOpen).toBe(false) + expect(group.isDelegating).toBe(false) + }) + it('prunes an empty nested subagent that started and ended without output', () => { const blocks: ContentBlock[] = [ subagentStart('workflow', 'S1', 'main'), diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 05cc1544389..356ea7e00ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -1,6 +1,6 @@ 'use client' -import { memo, useMemo } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { stripVersionSuffix } from '@sim/utils/string' import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' @@ -18,11 +18,14 @@ import { PendingTagIndicator, ThinkingBlock, } from './components' +import { deriveMessagePhase, isToolDone, type MessagePhase } from './utils' const FILE_SUBAGENT_ID = 'file' interface TextSegment { type: 'text' + /** Stable per-run React key (see the counters in parseBlocksWithSpanTree). */ + id: string content: string } @@ -93,17 +96,6 @@ function resolveAgentLabel(key: string): string { return SUBAGENT_LABELS[key] ?? formatToolName(key) } -function isToolDone(status: ToolCallData['status']): boolean { - return ( - status === 'success' || - status === 'error' || - status === 'cancelled' || - status === 'skipped' || - status === 'rejected' || - status === 'interrupted' - ) -} - function isDelegatingTool(tc: NonNullable): boolean { return tc.status === 'executing' } @@ -155,10 +147,10 @@ function toToolData(tc: NonNullable): ToolCallData { const SPAN_ROOT = 'main' -function createAgentGroupSegment(name: string, idKey: string, ordinal: number): AgentGroupSegment { +function createAgentGroupSegment(name: string, id: string): AgentGroupSegment { return { type: 'agent_group', - id: `agent-${idKey}-${ordinal}`, + id, agentName: name, agentLabel: resolveAgentLabel(name), items: [], @@ -187,6 +179,27 @@ function appendTextItem(group: AgentGroupSegment, content: string): void { function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { const segments: MessageSegment[] = [] const groupsBySpanId = new Map() + // Stable per-run counters for React keys. The Nth top-level text run / Nth + // mothership group keeps the same key across re-parses (text runs and groups + // are append-only at the top level), so React never remounts the streaming + // ChatContent / AgentGroup when later segments shift array position. Keying by + // array index or block index is unstable (subagent_end interleaves, parallel + // spans reorder), which caused the disappear/re-animate + parallel-subagent flash. + let textRun = 0 + let mothershipRun = 0 + + // Canonical subagent identity: the dispatch tool call id. It is stable across + // the no-spanId (legacy parser) -> spanId (span-tree parser) transition and + // across DB-load vs live, so the group's React key never changes when the + // underlying span id is stamped — eliminating the remount/flash and keeping a + // refreshed transcript byte-identical to the live stream. + const spanAnchor = new Map() + for (const b of blocks) { + if (b.type === 'subagent' && b.spanId && b.parentToolCallId) { + spanAnchor.set(b.spanId, b.parentToolCallId) + } + } + const spanGroupKey = (spanId: string): string => `agent-${spanAnchor.get(spanId) ?? spanId}` const tailMothershipGroup = (): AgentGroupSegment | null => { const last = segments[segments.length - 1] @@ -202,7 +215,7 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { const ensureMothership = (): AgentGroupSegment => { const existing = tailMothershipGroup() if (existing) return existing - const group = createAgentGroupSegment('mothership', 'mothership', segments.length) + const group = createAgentGroupSegment('mothership', `agent-mothership-${mothershipRun++}`) segments.push(group) return group } @@ -226,6 +239,7 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { if (parentSpanId && parentSpanId !== SPAN_ROOT) { const parent = groupsBySpanId.get(parentSpanId) if (parent) { + parent.isDelegating = false parent.items.push({ type: 'agent_group', group }) return } @@ -236,12 +250,13 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { const ensureSpanGroup = ( name: string, spanId: string, - parentSpanId: string | undefined, - ordinal: number + parentSpanId: string | undefined ): AgentGroupSegment => { const existing = groupsBySpanId.get(spanId) if (existing) return existing - const group = createAgentGroupSegment(name, spanId, ordinal) + // Key by the dispatch tool call id (canonical, parser-stable) when known, + // falling back to the spanId for spans with no dispatch tool (legacy/orphan). + const group = createAgentGroupSegment(name, spanGroupKey(spanId)) groupsBySpanId.set(spanId, group) attachSpanGroup(group, parentSpanId) return group @@ -252,7 +267,7 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { if (last?.type === 'text') { last.content += content } else { - segments.push({ type: 'text', content }) + segments.push({ type: 'text', id: `text-${textRun++}`, content }) } } @@ -266,7 +281,7 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { // (live streaming across resume legs). Create the span group on demand, // nested via parentSpanId, instead of dropping the content. if (!g && block.subagent) { - g = ensureSpanGroup(block.subagent, block.spanId, block.parentSpanId, i) + g = ensureSpanGroup(block.subagent, block.spanId, block.parentSpanId) } if (!g) continue g.isDelegating = false @@ -297,7 +312,7 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { if (block.subagent && block.spanId) { let g = groupsBySpanId.get(block.spanId) // Out-of-order safety: see subagent_text branch above. - if (!g) g = ensureSpanGroup(block.subagent, block.spanId, block.parentSpanId, i) + if (!g) g = ensureSpanGroup(block.subagent, block.spanId, block.parentSpanId) if (g) { g.isDelegating = false appendTextItem(g, block.content) @@ -314,7 +329,17 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { // not render as a separate entry alongside the agent group. const dispatchToolName = SUBAGENT_DISPATCH_TOOLS[block.content] if (dispatchToolName) absorbDispatchTool(dispatchToolName, block.parentSpanId) - const g = ensureSpanGroup(block.content, block.spanId, block.parentSpanId, i) + const g = ensureSpanGroup(block.content, block.spanId, block.parentSpanId) + if (block.endedAt !== undefined) { + // Persisted backend path: the lane was stamped closed (endedAt) without + // a separate subagent_end block (the Sim backend stamps endedAt only; + // only the live browser path pushes subagent_end). Honor endedAt so a + // reloaded transcript shows the subagent closed instead of a stuck + // delegating spinner. + g.isOpen = false + g.isDelegating = false + continue + } // Show the working/delegating spinner from span open until the agent // emits its first content or tool (or ends). The legacy path derived this // from the dispatch tool_call, which the span path absorbs, so we set it @@ -340,7 +365,7 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { // span group on demand (nested via parentSpanId) so the tool nests // under its agent instead of leaking to the top-level mothership flow. if (!g && tc.calledBy) { - g = ensureSpanGroup(tc.calledBy, block.spanId, block.parentSpanId, i) + g = ensureSpanGroup(tc.calledBy, block.spanId, block.parentSpanId) } if (g) { g.isDelegating = false @@ -444,7 +469,11 @@ function parseBlocksLegacy(blocks: ContentBlock[]): MessageSegment[] { if (existing) return { group: existing, created: false } const group: AgentGroupSegment = { type: 'agent_group', - id: `agent-${key}-${segments.length}`, + // Canonical key = the dispatch tool call id, identical to the span-tree + // parser, so a transcript that gains span ids (or a DB reload) keeps the + // same React key and never remounts. Orphans (no dispatch tool) keep the + // position-based legacy id. + id: parentToolCallId ? `agent-${parentToolCallId}` : `agent-${key}-${segments.length}`, agentName: name, agentLabel: resolveAgentLabel(name), items: [], @@ -534,7 +563,7 @@ function parseBlocksLegacy(blocks: ContentBlock[]): MessageSegment[] { if (last?.type === 'text') { last.content += block.content } else { - segments.push({ type: 'text', content: block.content }) + segments.push({ type: 'text', id: `text-${i}`, content: block.content }) } continue } @@ -656,7 +685,7 @@ export function assistantMessageHasRenderableContent( parsed.length > 0 ? parsed : fallbackContent.trim() - ? [{ type: 'text' as const, content: fallbackContent }] + ? [{ type: 'text' as const, id: 'text-fallback', content: fallbackContent }] : [] return segments.length > 0 } @@ -678,6 +707,7 @@ interface MessageContentProps { fallbackContent: string isStreaming: boolean onOptionSelect?: (id: string) => void + onPhaseChange?: (phase: MessagePhase) => void } function MessageContentInner({ @@ -685,17 +715,34 @@ function MessageContentInner({ fallbackContent, isStreaming = false, onOptionSelect, + onPhaseChange, }: MessageContentProps) { const { onWorkspaceResourceSelect } = useChatSurface() const parsed = useMemo(() => (blocks.length > 0 ? parseBlocks(blocks) : []), [blocks]) + const [trailingRevealing, setTrailingRevealing] = useState(false) + const handleTrailingRevealChange = useCallback((revealing: boolean) => { + setTrailingRevealing(revealing) + }, []) + const segments: MessageSegment[] = parsed.length > 0 ? parsed : fallbackContent?.trim() - ? [{ type: 'text' as const, content: fallbackContent }] + ? [{ type: 'text' as const, id: 'text-fallback', content: fallbackContent }] : [] + const lastSegment = segments[segments.length - 1] + const hasTrailingTextSegment = lastSegment?.type === 'text' + const isRevealing = hasTrailingTextSegment && trailingRevealing + const phase = deriveMessagePhase({ isStreaming, isRevealing }) + + const onPhaseChangeRef = useRef(onPhaseChange) + onPhaseChangeRef.current = onPhaseChange + useEffect(() => { + onPhaseChangeRef.current?.(phase) + }, [phase]) + if (segments.length === 0) { if (isStreaming) { return ( @@ -707,21 +754,16 @@ function MessageContentInner({ return null } - const lastSegment = segments[segments.length - 1] const hasTrailingContent = lastSegment.type === 'text' || lastSegment.type === 'stopped' - let allLastGroupToolsDone = false - if (lastSegment.type === 'agent_group') { - const toolItems = lastSegment.items.filter((item) => item.type === 'tool') - allLastGroupToolsDone = - toolItems.length > 0 && toolItems.every((t) => t.type === 'tool' && isToolDone(t.data.status)) - } - - const hasSubagentEnded = blocks.some((b) => b.type === 'subagent_end') - const showTrailingThinking = - isStreaming && - !hasTrailingContent && - (lastSegment.type === 'thinking' || hasSubagentEnded || allLastGroupToolsDone) + // Deterministic "between steps" signal: the turn is still streaming, nothing + // is actively running (a running tool/subagent renders its own spinner), and + // no trailing text is being revealed. Derived from explicit node state rather + // than guessing from the shape of the last segment. + const hasRunningWork = blocks.some( + (b) => b.toolCall?.status === 'executing' || (b.type === 'subagent' && b.endedAt === undefined) + ) + const showTrailingThinking = phase === 'streaming' && !hasTrailingContent && !hasRunningWork return (
@@ -730,7 +772,7 @@ function MessageContentInner({ case 'text': return ( ) case 'thinking': { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts new file mode 100644 index 00000000000..f0a74e539a7 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts @@ -0,0 +1,38 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { deriveMessagePhase, resolveToolDisplayState } from './utils' + +describe('deriveMessagePhase', () => { + it('is streaming whenever the transport is live', () => { + expect(deriveMessagePhase({ isStreaming: true, isRevealing: false })).toBe('streaming') + expect(deriveMessagePhase({ isStreaming: true, isRevealing: true })).toBe('streaming') + }) + + it('is revealing when the transport stopped but text is still draining', () => { + expect(deriveMessagePhase({ isStreaming: false, isRevealing: true })).toBe('revealing') + }) + + it('is settled once neither the transport nor the reveal is active', () => { + expect(deriveMessagePhase({ isStreaming: false, isRevealing: false })).toBe('settled') + }) +}) + +describe('resolveToolDisplayState', () => { + it('spins iff the tool is executing — a pure projection of its own status', () => { + expect(resolveToolDisplayState('executing')).toBe('spinner') + }) + + it('maps cancelled and interrupted to their own glyphs', () => { + expect(resolveToolDisplayState('cancelled')).toBe('cancelled') + expect(resolveToolDisplayState('interrupted')).toBe('interrupted') + }) + + it('renders terminal successes and errors as the tool icon', () => { + expect(resolveToolDisplayState('success')).toBe('icon') + expect(resolveToolDisplayState('error')).toBe('icon') + expect(resolveToolDisplayState('skipped')).toBe('icon') + expect(resolveToolDisplayState('rejected')).toBe('icon') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts index b5e05c3e794..7cf99bbf16e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts @@ -21,6 +21,7 @@ import { } from '@/components/emcn' import { Calendar, Table as TableIcon } from '@/components/emcn/icons' import { AgentIcon, ImageIcon, TTSIcon, VideoIcon } from '@/components/icons' +import type { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' export type IconComponent = ComponentType> @@ -74,3 +75,43 @@ export function getToolIcon(name: string): IconComponent | undefined { const icon = TOOL_ICONS[name as keyof typeof TOOL_ICONS] return icon === Blimp ? undefined : icon } + +export type MessagePhase = 'streaming' | 'revealing' | 'settled' + +interface DeriveMessagePhaseArgs { + isStreaming: boolean + isRevealing: boolean +} + +export function deriveMessagePhase({ + isStreaming, + isRevealing, +}: DeriveMessagePhaseArgs): MessagePhase { + if (isStreaming) return 'streaming' + if (isRevealing) return 'revealing' + return 'settled' +} + +type ToolDisplayState = 'spinner' | 'cancelled' | 'interrupted' | 'icon' + +export function resolveToolDisplayState(status: ToolCallStatus): ToolDisplayState { + // Pure projection of the tool's own status. A row spins iff it is genuinely + // executing; every terminal status maps to a glyph. No transport/turn-live + // gating — deterministic terminals (tool `result`, turn propagation) guarantee + // a row never lingers `executing` after its work is done. + if (status === 'executing') return 'spinner' + if (status === 'cancelled') return 'cancelled' + if (status === 'interrupted') return 'interrupted' + return 'icon' +} + +export function isToolDone(status: ToolCallStatus): boolean { + return ( + status === 'success' || + status === 'error' || + status === 'cancelled' || + status === 'skipped' || + status === 'rejected' || + status === 'interrupted' + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index e13bab2f1a8..3eb686cb883 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -1,6 +1,15 @@ 'use client' -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react' +import { + memo, + useCallback, + useDeferredValue, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' import { defaultRangeExtractor, type Range, useVirtualizer } from '@tanstack/react-virtual' import { cn } from '@/lib/core/utils/cn' import { MessageActions } from '@/app/workspace/[workspaceId]/components' @@ -9,6 +18,7 @@ import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/componen import { assistantMessageHasRenderableContent, MessageContent, + type MessagePhase, } from '@/app/workspace/[workspaceId]/home/components/message-content' import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages' @@ -155,6 +165,7 @@ interface AssistantMessageRowProps { precedingUserContent?: string rowClassName: string onOptionSelect?: (id: string) => void + onAnimatingChange?: (animating: boolean) => void } const AssistantMessageRow = memo(function AssistantMessageRow({ @@ -163,11 +174,20 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ precedingUserContent, rowClassName, onOptionSelect, + onAnimatingChange, }: AssistantMessageRowProps) { const blocks = message.contentBlocks ?? EMPTY_BLOCKS const hasAnyBlocks = blocks.length > 0 const trimmedContent = message.content?.trim() ?? '' + const [phase, setPhase] = useState(isStreaming ? 'streaming' : 'settled') + + const onAnimatingChangeRef = useRef(onAnimatingChange) + onAnimatingChangeRef.current = onAnimatingChange + useEffect(() => { + onAnimatingChangeRef.current?.(phase !== 'settled') + }, [phase]) + if (!hasAnyBlocks && !trimmedContent && isStreaming) { return } @@ -177,7 +197,7 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ return null } - const showActions = !isStreaming && (message.content || hasAnyBlocks) + const showActions = phase === 'settled' && (message.content || hasAnyBlocks) return (
@@ -186,6 +206,7 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ fallbackContent={message.content} isStreaming={isStreaming} onOptionSelect={onOptionSelect} + onPhaseChange={setPhase} /> {showActions && (
@@ -202,7 +223,7 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ }) export function MothershipChat({ - messages, + messages: messagesProp, isSending, isReconnecting = false, isLoading = false, @@ -229,8 +250,16 @@ export function MothershipChat({ }: MothershipChatProps) { const styles = LAYOUT_STYLES[layout] const isStreamActive = isSending || isReconnecting + /** + * Defer the streamed message list so its re-render (virtualizer + rows) is + * low-priority: React yields it to urgent interactions (dragging/panning the + * side-panel canvas, scrolling, typing), keeping those at 60fps instead of + * starving the main thread on every streaming token. + */ + const messages = useDeferredValue(messagesProp) + const [lastRowAnimating, setLastRowAnimating] = useState(false) const scrollElementRef = useRef(null) - const { ref: autoScrollRef } = useAutoScroll(isStreamActive) + const { ref: autoScrollRef } = useAutoScroll(isStreamActive || lastRowAnimating) const setScrollElement = useCallback( (el: HTMLDivElement | null) => { scrollElementRef.current = el @@ -282,6 +311,11 @@ export function MothershipChat({ * one extra always-mounted row. */ const lastIndex = messages.length - 1 + const lastRowKey = lastIndex >= 0 ? rowKeyByIndex[lastIndex] : undefined + useEffect(() => { + setLastRowAnimating(false) + }, [lastRowKey]) + const rangeExtractor = useCallback( (range: Range) => { const indexes = defaultRangeExtractor(range) @@ -405,6 +439,7 @@ export function MothershipChat({ precedingUserContent={precedingUserContentByIndex[index]} rowClassName={cn(styles.assistantRow, styles.rowGap)} onOptionSelect={isLast ? stableOnOptionSelect : undefined} + onAnimatingChange={isLast ? setLastRowAnimating : undefined} /> )}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.test.ts index 834abbf108c..b0761dc9180 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.test.ts @@ -43,14 +43,13 @@ vi.mock('@/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event', })) import { dispatchStreamEvent } from './dispatch-stream-event' +import { createTurnModel } from './turn-model' function makeCtx(): StreamLoopContext { return { - state: {} as StreamLoopContext['state'], + state: { model: createTurnModel() } as StreamLoopContext['state'], deps: {} as StreamLoopContext['deps'], - ops: { - resolveScopedSubagent: vi.fn(() => 'sub-agent'), - } as unknown as StreamLoopContext['ops'], + ops: {} as unknown as StreamLoopContext['ops'], } } @@ -92,20 +91,17 @@ describe('dispatchStreamEvent', () => { expect(handlers.handleCompleteEvent).toHaveBeenCalledTimes(1) }) - it('computes and passes per-event scope to scoped handlers', () => { + it('computes and passes per-event scope to the span handler', () => { const ctx = makeCtx() dispatchStreamEvent( ctx, - event(MothershipStreamV1EventType.text, { spanId: 'span-9', agentId: 'agent-x' }) + event(MothershipStreamV1EventType.span, { spanId: 'span-9', agentId: 'agent-x' }) ) - expect(ctx.ops.resolveScopedSubagent).toHaveBeenCalledWith('agent-x', undefined, 'span-9') - const call = handlers.handleTextEvent.mock.calls[0] + const call = handlers.handleSpanEvent.mock.calls[0] expect(call[0]).toBe(ctx) const scope = call[2] expect(scope.scopedSpanId).toBe('span-9') expect(scope.scopedAgentId).toBe('agent-x') - expect(scope.scopedSubagent).toBe('sub-agent') - expect(scope.spanIdentity).toEqual({ spanId: 'span-9' }) }) it('invokes ctx-only handlers (session/run/complete) without a scope argument', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.ts index dace38d8e90..7d3de4ba00e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.ts @@ -12,56 +12,45 @@ import type { StreamEventScope, StreamLoopContext, } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' +import { reduceEvent } from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model' -function computeEventScope( - ctx: StreamLoopContext, - parsed: PersistedStreamEventEnvelope -): StreamEventScope { - const scopedParentToolCallId = - typeof parsed.scope?.parentToolCallId === 'string' ? parsed.scope.parentToolCallId : undefined - const scopedAgentId = typeof parsed.scope?.agentId === 'string' ? parsed.scope.agentId : undefined - const scopedSpanId = typeof parsed.scope?.spanId === 'string' ? parsed.scope.spanId : undefined - const scopedParentSpanId = - typeof parsed.scope?.parentSpanId === 'string' ? parsed.scope.parentSpanId : undefined - const scopedSubagent = ctx.ops.resolveScopedSubagent( - scopedAgentId, - scopedParentToolCallId, - scopedSpanId - ) - const spanIdentity: { spanId?: string; parentSpanId?: string } = { - ...(scopedSpanId ? { spanId: scopedSpanId } : {}), - ...(scopedParentSpanId ? { parentSpanId: scopedParentSpanId } : {}), - } +// The model owns subagent attribution by scope identity; only the span handler +// needs scope, and only these three fields (agent id for file-preview seeding, +// span/parent ids for lane identity). +function computeEventScope(parsed: PersistedStreamEventEnvelope): StreamEventScope { return { - scopedSubagent, - scopedParentToolCallId, - scopedAgentId, - scopedSpanId, - scopedParentSpanId, - spanIdentity, + scopedParentToolCallId: + typeof parsed.scope?.parentToolCallId === 'string' + ? parsed.scope.parentToolCallId + : undefined, + scopedAgentId: typeof parsed.scope?.agentId === 'string' ? parsed.scope.agentId : undefined, + scopedSpanId: typeof parsed.scope?.spanId === 'string' ? parsed.scope.spanId : undefined, } } /** - * Routes a parsed stream event to its handler. Per-event subagent/span scope is - * resolved once here and passed to the handlers that nest blocks by it. The - * caller's transport loop owns staleness, cursor dedup, and `streamId`/ - * `streamRequestId` updates; this function only mutates the supplied context. + * Folds a parsed stream event into the model (the single source of truth), then + * routes it to its side-effect handler. Span scope is computed only for the span + * handler (handlers no longer nest blocks — the model does). The caller's + * transport loop owns staleness, cursor dedup, and `streamId`/`streamRequestId`. */ export function dispatchStreamEvent( ctx: StreamLoopContext, parsed: PersistedStreamEventEnvelope ): void { - const scope = computeEventScope(ctx, parsed) + // The model is the single source of truth: fold every event into it first, + // then run the handlers for their side effects (resource/query/preview) and + // the snapshot flush, which serializes the model. + reduceEvent(ctx.state.model, parsed) switch (parsed.type) { case MothershipStreamV1EventType.session: handleSessionEvent(ctx, parsed) break case MothershipStreamV1EventType.text: - handleTextEvent(ctx, parsed, scope) + handleTextEvent(ctx, parsed) break case MothershipStreamV1EventType.tool: - handleToolEvent(ctx, parsed, scope) + handleToolEvent(ctx, parsed) break case MothershipStreamV1EventType.resource: handleResourceEvent(ctx, parsed) @@ -70,10 +59,10 @@ export function dispatchStreamEvent( handleRunEvent(ctx, parsed) break case MothershipStreamV1EventType.span: - handleSpanEvent(ctx, parsed, scope) + handleSpanEvent(ctx, parsed, computeEventScope(parsed)) break case MothershipStreamV1EventType.error: - handleErrorEvent(ctx, parsed, scope) + handleErrorEvent(ctx, parsed) break case MothershipStreamV1EventType.complete: handleCompleteEvent(ctx, parsed) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts index 2c6254ac7c6..c4eb9889811 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts @@ -1,25 +1,14 @@ -import { MothershipStreamV1CompletionStatus } from '@/lib/copilot/generated/mothership-stream-v1' import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' import type { StreamLoopContext } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' -import { - asPayloadRecord, - finalizeResidualToolCalls, -} from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers' type CompleteEvent = Extract -export function handleCompleteEvent(ctx: StreamLoopContext, parsed: CompleteEvent): void { - const { state, ops } = ctx - state.sawCompleteEvent = true - ops.stampBlockEnd(state.blocks[state.blocks.length - 1]) - const completeResponse = asPayloadRecord(parsed.payload.response) - if (completeResponse === undefined || !('async_pause' in completeResponse)) { - finalizeResidualToolCalls( - state.blocks, - parsed.payload.status === MothershipStreamV1CompletionStatus.cancelled - ? 'cancelled' - : 'complete' - ) - ops.flush() - } +/** + * Turn termination and the deterministic propagation of the outcome to any + * still-open node are folded into the model by `reduceEvent` (which skips an + * async pause). This handler only records the terminal flag and flushes. + */ +export function handleCompleteEvent(ctx: StreamLoopContext, _parsed: CompleteEvent): void { + ctx.state.sawCompleteEvent = true + ctx.ops.flush() } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-error-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-error-event.ts index 6c1d00f42bf..54b11258412 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-error-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-error-event.ts @@ -1,23 +1,16 @@ import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' -import type { - StreamEventScope, - StreamLoopContext, -} from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' +import type { StreamLoopContext } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' type ErrorEvent = Extract -export function handleErrorEvent( - ctx: StreamLoopContext, - parsed: ErrorEvent, - scope: StreamEventScope -): void { +/** + * The inline error tag is folded into the model by `reduceEvent` (scoped to the + * erroring lane). This handler owns the side effects: flag the stream error and + * surface the message, then flush the serialized snapshot. + */ +export function handleErrorEvent(ctx: StreamLoopContext, parsed: ErrorEvent): void { const { state, ops, deps } = ctx state.sawStreamError = true deps.setError(parsed.payload.message || parsed.payload.error || 'An error occurred') - ops.appendInlineErrorTag( - ops.buildInlineErrorTag(parsed.payload), - scope.scopedSubagent, - ops.resolveParentForSubagentBlock(scope.scopedSubagent, scope.scopedParentToolCallId), - typeof parsed.ts === 'string' ? parsed.ts : undefined - ) + ops.flush() } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-run-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-run-event.ts index f8c0d2df81d..4182dd92302 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-run-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-run-event.ts @@ -1,55 +1,13 @@ -import { MothershipStreamV1RunKind } from '@/lib/copilot/generated/mothership-stream-v1' import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' import type { StreamLoopContext } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' type RunEvent = Extract -export function handleRunEvent(ctx: StreamLoopContext, parsed: RunEvent): void { - const { state, ops } = ctx - const payload = parsed.payload - - if (payload.kind === MothershipStreamV1RunKind.compaction_start) { - const compactionId = `compaction_${Date.now()}` - state.activeCompactionId = compactionId - ops.stampBlockEnd(state.blocks[state.blocks.length - 1]) - state.toolMap.set(compactionId, state.blocks.length) - state.blocks.push({ - type: 'tool_call', - toolCall: { - id: compactionId, - name: 'context_compaction', - status: 'executing', - displayTitle: 'Compacting context...', - }, - timestamp: Date.now(), - }) - ops.flush() - return - } - - if (payload.kind === MothershipStreamV1RunKind.compaction_done) { - const compactionId = state.activeCompactionId || `compaction_${Date.now()}` - state.activeCompactionId = undefined - const idx = state.toolMap.get(compactionId) - if (idx !== undefined && state.blocks[idx]?.toolCall) { - state.blocks[idx].toolCall!.status = 'success' - state.blocks[idx].toolCall!.displayTitle = 'Compacted context' - ops.stampBlockEnd(state.blocks[idx]) - } else { - state.toolMap.set(compactionId, state.blocks.length) - const endNow = Date.now() - state.blocks.push({ - type: 'tool_call', - toolCall: { - id: compactionId, - name: 'context_compaction', - status: 'success', - displayTitle: 'Compacted context', - }, - timestamp: endNow, - endedAt: endNow, - }) - } - ops.flush() - } +/** + * Compaction lifecycle is folded into the model by `reduceEvent` (it opens and + * closes a `context_compaction` node with titles). This handler only flushes the + * serialized snapshot. + */ +export function handleRunEvent(ctx: StreamLoopContext, _parsed: RunEvent): void { + ctx.ops.flush() } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-span-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-span-event.ts index 49db0877be5..7c0902470da 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-span-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-span-event.ts @@ -14,13 +14,19 @@ import { type SpanEvent = Extract +/** + * Side effects for subagent span lifecycle. The model owns the subagent + * group/nesting/close (via `reduceEvent`); this handler only seeds the file + * preview session on a fresh file-subagent start and reconciles the file + * resource chrome on end, then flushes the model-derived snapshot. + */ export function handleSpanEvent( ctx: StreamLoopContext, parsed: SpanEvent, scope: StreamEventScope ): void { const { state, ops, deps } = ctx - const { scopedParentToolCallId, scopedAgentId, scopedSpanId, spanIdentity } = scope + const { scopedParentToolCallId, scopedAgentId, scopedSpanId } = scope const payload = parsed.payload if (payload.kind !== MothershipStreamV1SpanPayloadKind.subagent) { return @@ -36,37 +42,12 @@ export function handleSpanEvent( const isPendingPause = spanData?.pending === true const name = typeof payload.agent === 'string' ? payload.agent : scopedAgentId - if (payload.event === MothershipStreamV1SpanLifecycleEvent.start && name) { - const existingOpenForSpan = scopedSpanId - ? state.blocks.some( - (b) => b.type === 'subagent' && b.spanId === scopedSpanId && b.endedAt === undefined - ) - : false - const isSameActiveSubagent = - existingOpenForSpan || - (!scopedSpanId && - state.activeSubagent === name && - Boolean(state.activeSubagentParentToolCallId) && - parentToolCallId === state.activeSubagentParentToolCallId) - if (scopedSpanId) { - state.subagentBySpanId.set(scopedSpanId, name) - } - if (parentToolCallId) { - state.subagentByParentToolCallId.set(parentToolCallId, name) - } - state.activeSubagent = name - state.activeSubagentParentToolCallId = parentToolCallId - if (!isSameActiveSubagent) { - ops.stampBlockEnd(state.blocks[state.blocks.length - 1]) - state.blocks.push({ - type: 'subagent', - content: name, - ...(parentToolCallId ? { parentToolCallId } : {}), - ...spanIdentity, - timestamp: Date.now(), - }) - } - if (name === FILE_SUBAGENT_ID && !isSameActiveSubagent) { + if (payload.event === MothershipStreamV1SpanLifecycleEvent.start && name === FILE_SUBAGENT_ID) { + // Seed the pending preview session only on a freshly-opened lane (the agent + // node was created by this event), so concurrent file subagents don't re-seed. + const node = scopedSpanId ? state.model.nodes.get(scopedSpanId) : undefined + const isNewLane = node?.kind === 'agent' && node.seq === parsed.seq + if (isNewLane) { deps.applyPreviewSessionUpdate({ schemaVersion: 1, id: parentToolCallId || 'file-preview', @@ -87,12 +68,6 @@ export function handleSpanEvent( if (isPendingPause) { return } - if (scopedSpanId) { - state.subagentBySpanId.delete(scopedSpanId) - } - if (parentToolCallId) { - state.subagentByParentToolCallId.delete(parentToolCallId) - } if ( deps.previewSessionRef.current && (!deps.activePreviewSessionIdRef.current || @@ -106,44 +81,6 @@ export function handleSpanEvent( deps.setActiveResourceId(lastFileResource.id) } } - if ( - !parentToolCallId || - parentToolCallId === state.activeSubagentParentToolCallId || - name === state.activeSubagent - ) { - state.activeSubagent = undefined - state.activeSubagentParentToolCallId = undefined - } - const endNow = Date.now() - if (scopedSpanId) { - for (let i = state.blocks.length - 1; i >= 0; i--) { - const b = state.blocks[i] - if (b.type === 'subagent' && b.spanId === scopedSpanId && b.endedAt === undefined) { - b.endedAt = endNow - break - } - } - } else if (name) { - for (let i = state.blocks.length - 1; i >= 0; i--) { - const b = state.blocks[i] - if ( - b.type === 'subagent' && - b.content === name && - b.endedAt === undefined && - (!parentToolCallId || b.parentToolCallId === parentToolCallId) - ) { - b.endedAt = endNow - break - } - } - } - ops.stampBlockEnd(state.blocks[state.blocks.length - 1]) - state.blocks.push({ - type: 'subagent_end', - ...(parentToolCallId ? { parentToolCallId } : {}), - ...spanIdentity, - timestamp: endNow, - }) ops.flush() } } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-text-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-text-event.ts index 766c0aa1fe9..ba115ca97d6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-text-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-text-event.ts @@ -1,51 +1,13 @@ -import { MothershipStreamV1TextChannel } from '@/lib/copilot/generated/mothership-stream-v1' import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' -import type { - StreamEventScope, - StreamLoopContext, -} from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' +import type { StreamLoopContext } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' type TextEvent = Extract -export function handleTextEvent( - ctx: StreamLoopContext, - parsed: TextEvent, - scope: StreamEventScope -): void { - const { state, ops, deps } = ctx - const { scopedSubagent, scopedParentToolCallId, spanIdentity } = scope - - const chunk = parsed.payload.text - if (!chunk) return - - const eventTs = typeof parsed.ts === 'string' ? parsed.ts : undefined - - if (parsed.payload.channel === MothershipStreamV1TextChannel.thinking) { - const scopedParentForBlock = ops.resolveParentForSubagentBlock( - scopedSubagent, - scopedParentToolCallId - ) - const tb = ops.ensureThinkingBlock(scopedSubagent, scopedParentForBlock, eventTs, spanIdentity) - tb.content = (tb.content ?? '') + chunk - ops.flushText() - return - } - - const contentSource: 'main' | 'subagent' = scopedSubagent ? 'subagent' : 'main' - const needsBoundaryNewline = - state.lastContentSource !== null && - state.lastContentSource !== contentSource && - state.runningText.length > 0 && - !state.runningText.endsWith('\n') - const scopedParentForBlock = ops.resolveParentForSubagentBlock( - scopedSubagent, - scopedParentToolCallId - ) - const tb = ops.ensureTextBlock(scopedSubagent, scopedParentForBlock, eventTs, spanIdentity) - const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk - tb.content = (tb.content ?? '') + normalizedChunk - state.runningText += normalizedChunk - state.lastContentSource = contentSource - deps.streamingContentRef.current = state.runningText - ops.flushText() +/** + * Text content is folded into the model by `reduceEvent` (main and subagent + * lanes are kept distinct, so there is no manual boundary-newline). This handler + * only schedules a paced flush of the serialized snapshot. + */ +export function handleTextEvent(ctx: StreamLoopContext, _parsed: TextEvent): void { + ctx.ops.flushText() } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.test.ts index 0dd762aa3a4..1a760f149f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.test.ts @@ -7,9 +7,6 @@ vi.mock('@/lib/copilot/resources/extraction', () => ({ isResourceToolName: vi.fn(() => false), extractResourcesFromToolResult: vi.fn(() => []), })) -vi.mock('@/lib/copilot/tools/client/hidden-tools', () => ({ - isToolHiddenInUi: vi.fn(() => false), -})) vi.mock('@/lib/copilot/tools/workflow-tools', () => ({ isWorkflowToolName: vi.fn(() => false), })) @@ -18,80 +15,112 @@ vi.mock( () => ({ invalidateResourceQueries: vi.fn() }) ) -import { handleToolEvent } from './handle-tool-event' -import { createStreamLoopContext, type StreamEventScope } from './stream-context' -import { makeStreamLoopDeps } from './stream-test-helpers' - -const SCOPE: StreamEventScope = { - scopedSubagent: undefined, - scopedParentToolCallId: undefined, - scopedAgentId: undefined, - scopedSpanId: undefined, - scopedParentSpanId: undefined, - spanIdentity: {}, -} - -type ToolEvent = Parameters[1] +import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' +import type { FilePreviewSession } from '@/lib/copilot/request/session/file-preview-session-contract' +import { dispatchStreamEvent } from './dispatch-stream-event' +import { createStreamLoopContext, type StreamLoopContext } from './stream-context' +import { makeStreamLoopDeps, ref } from './stream-test-helpers' +import type { ToolNode } from './turn-model' -function toolCall(id: string, name = 'my_tool'): ToolEvent { +let seq = 0 +function toolEnv(payload: Record): PersistedStreamEventEnvelope { return { type: 'tool', v: 1, - seq: 1, - ts: '2026-01-01T00:00:00Z', - stream: { streamId: 's' }, - payload: { phase: 'call', executor: 'go', mode: 'sync', toolCallId: id, toolName: name }, - } as unknown as ToolEvent + seq: ++seq, + ts: '', + stream: { streamId: 's', cursor: String(seq) }, + payload, + } as unknown as PersistedStreamEventEnvelope } -function toolResult(id: string, success: boolean, name = 'my_tool'): ToolEvent { +const toolCall = (id: string, name = 'my_tool') => + toolEnv({ phase: 'call', executor: 'go', mode: 'sync', toolCallId: id, toolName: name }) + +const toolResult = (id: string, success: boolean, name = 'my_tool') => + toolEnv({ + phase: 'result', + executor: 'go', + mode: 'sync', + toolCallId: id, + toolName: name, + success, + status: success ? 'success' : 'error', + }) + +const workspaceFileCall = (id: string) => + toolEnv({ + phase: 'call', + executor: 'sim', + mode: 'async', + toolCallId: id, + toolName: 'workspace_file', + arguments: { operation: 'append', target: { kind: 'file_id', fileId: 'f1' } }, + }) + +const filePreviewComplete = (id: string) => + toolEnv({ previewPhase: 'file_preview_complete', toolCallId: id, toolName: 'workspace_file' }) + +function streamingSession(toolCallId: string): FilePreviewSession { return { - type: 'tool', - v: 1, - seq: 2, - ts: '2026-01-01T00:00:01Z', - stream: { streamId: 's' }, - payload: { - phase: 'result', - executor: 'go', - mode: 'sync', - toolCallId: id, - toolName: name, - success, - status: success ? 'success' : 'error', - }, - } as unknown as ToolEvent + schemaVersion: 1, + id: toolCallId, + streamId: 's', + toolCallId, + status: 'streaming', + fileName: 'doc.md', + previewText: 'hello', + previewVersion: 1, + updatedAt: '', + } } -describe('handleToolEvent', () => { - it('adds an executing tool_call block on a new call and resolves it on the result', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - handleToolEvent(ctx, toolCall('tc-1'), SCOPE) - expect(ctx.state.blocks).toHaveLength(1) - expect(ctx.state.blocks[0].toolCall?.id).toBe('tc-1') - expect(ctx.state.blocks[0].toolCall?.status).toBe('executing') - - handleToolEvent(ctx, toolResult('tc-1', true), SCOPE) - expect(ctx.state.blocks[0].toolCall?.status).toBe('success') - expect(ctx.state.blocks[0].endedAt).toBeTypeOf('number') +function toolNode(ctx: StreamLoopContext, id: string): ToolNode { + const node = ctx.state.model.nodes.get(id) + expect(node?.kind).toBe('tool') + return node as ToolNode +} + +describe('tool events (dispatch → model + side effects)', () => { + it('runs a tool then settles success, firing the onToolResult side effect', () => { + const onToolResult = vi.fn() + const ctx = createStreamLoopContext(makeStreamLoopDeps({ onToolResultRef: ref(onToolResult) })) + dispatchStreamEvent(ctx, toolCall('tc-1')) + expect(toolNode(ctx, 'tc-1').status).toBe('running') + + dispatchStreamEvent(ctx, toolResult('tc-1', true)) + expect(toolNode(ctx, 'tc-1').status).toBe('success') + expect(onToolResult).toHaveBeenCalledWith('my_tool', true, undefined) }) - it('buffers a result that arrives before its call, then applies it when the call lands', () => { + it('buffers a result that arrives before its call, then applies it', () => { const ctx = createStreamLoopContext(makeStreamLoopDeps()) - handleToolEvent(ctx, toolResult('tc-2', true), SCOPE) - expect(ctx.state.blocks).toHaveLength(0) - expect(ctx.state.pendingToolResults.has('tc-2')).toBe(true) - - handleToolEvent(ctx, toolCall('tc-2'), SCOPE) - expect(ctx.state.blocks).toHaveLength(1) - expect(ctx.state.blocks[0].toolCall?.status).toBe('success') - expect(ctx.state.pendingToolResults.has('tc-2')).toBe(false) + dispatchStreamEvent(ctx, toolResult('tc-2', true)) + expect(ctx.state.model.nodes.has('tc-2')).toBe(false) + + dispatchStreamEvent(ctx, toolCall('tc-2')) + expect(toolNode(ctx, 'tc-2').status).toBe('success') }) it('marks an unsuccessful result as error', () => { const ctx = createStreamLoopContext(makeStreamLoopDeps()) - handleToolEvent(ctx, toolCall('tc-3'), SCOPE) - handleToolEvent(ctx, toolResult('tc-3', false), SCOPE) - expect(ctx.state.blocks[0].toolCall?.status).toBe('error') + dispatchStreamEvent(ctx, toolCall('tc-3')) + dispatchStreamEvent(ctx, toolResult('tc-3', false)) + expect(toolNode(ctx, 'tc-3').status).toBe('error') + }) + + it('settles a file-write row on its own result, independent of a streaming preview session', () => { + const previewSessionsRef = ref>({}) + const ctx = createStreamLoopContext(makeStreamLoopDeps({ previewSessionsRef })) + dispatchStreamEvent(ctx, workspaceFileCall('wf-1')) + expect(toolNode(ctx, 'wf-1').status).toBe('running') + + previewSessionsRef.current['wf-1'] = streamingSession('wf-1') + dispatchStreamEvent(ctx, toolResult('wf-1', true, 'workspace_file')) + expect(toolNode(ctx, 'wf-1').status).toBe('success') + + // A later file_preview_complete is a preview-only signal; the tool row stays settled. + dispatchStreamEvent(ctx, filePreviewComplete('wf-1')) + expect(toolNode(ctx, 'wf-1').status).toBe('success') }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts index 20e36e0b145..1b2004a9a3f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts @@ -1,5 +1,4 @@ import { - MothershipStreamV1ToolOutcome, MothershipStreamV1ToolPhase, MothershipStreamV1ToolStatus, } from '@/lib/copilot/generated/mothership-stream-v1' @@ -9,111 +8,88 @@ import { extractResourcesFromToolResult, isResourceToolName, } from '@/lib/copilot/resources/extraction' -import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools' import { invalidateResourceQueries } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' -import type { - StreamEventScope, - StreamLoopContext, -} from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' +import type { StreamLoopContext } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' import { - asPayloadRecord, DEPLOY_TOOL_NAMES, extractResourceFromReadResult, FILE_SUBAGENT_ID, FOLDER_TOOL_NAMES, - getToolUI, - isTerminalToolCallStatus, - resolveLiveToolStatus, - resolveStreamingToolDisplayTitle, - resolveToolDisplayTitle, - type ToolResultPhasePayload, WORKFLOW_MUTATION_TOOL_NAMES, } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers' -import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' +import { + MAIN_SPAN, + resolveToolId, + type ToolNode, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model' import { deploymentKeys } from '@/hooks/queries/deployments' import { folderKeys } from '@/hooks/queries/utils/folder-keys' import { workflowKeys } from '@/hooks/queries/workflows' type ToolEvent = Extract -function applyToolResult( - ctx: StreamLoopContext, - idx: number, - id: string, - payload: ToolResultPhasePayload -): void { - const { state, ops, deps } = ctx - const tc = state.blocks[idx].toolCall! - const outputObj = asPayloadRecord(payload.output) - const isCancelled = - outputObj?.reason === 'user_cancelled' || - outputObj?.cancelledByUser === true || - payload.status === MothershipStreamV1ToolOutcome.cancelled - const status = isCancelled ? ToolCallStatus.cancelled : resolveLiveToolStatus(payload) - const isSuccess = status === ToolCallStatus.success - - if (status === ToolCallStatus.cancelled) { - tc.status = ToolCallStatus.cancelled - tc.displayTitle = 'Stopped by user' - } else { - tc.status = status - } - tc.streamingArgs = undefined - tc.result = { - success: isSuccess, - output: payload.output, - error: typeof payload.error === 'string' ? payload.error : undefined, - } - ops.stampBlockEnd(state.blocks[idx]) - ops.flush() +/** The display agent id for a tool's owning span (undefined on the main lane). */ +function agentIdForSpan(ctx: StreamLoopContext, spanId: string): string | undefined { + if (spanId === MAIN_SPAN) return undefined + const agent = ctx.state.model.nodes.get(spanId) + return agent?.kind === 'agent' ? agent.agentId : undefined +} - if (tc.name === ReadTool.id && tc.status === 'success') { - const readArgs = state.toolArgsMap.get(id) +/** + * Runs the external side effects of a finished tool (resource extraction, query + * invalidation, file-resource promotion, preview cleanup, onToolResult). The + * tool's lifecycle/status is owned by the model; this reads the settled node and + * only performs side effects, so the model stays the single source of state. + */ +function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode): void { + const { deps } = ctx + const name = node.name + const output = node.result?.output + const isSuccess = node.status === 'success' + const params = node.args + const calledBy = agentIdForSpan(ctx, node.spanId) + + if (name === ReadTool.id && isSuccess) { const resource = extractResourceFromReadResult( - typeof readArgs?.path === 'string' ? readArgs.path : undefined, - tc.result.output + typeof params?.path === 'string' ? params.path : undefined, + output ) if (resource && deps.addResource(resource)) { deps.onResourceEventRef.current?.() } } - if (DEPLOY_TOOL_NAMES.has(tc.name) && tc.status === 'success') { - const output = tc.result?.output as Record | undefined - const deployedWorkflowId = (output?.workflowId as string) ?? undefined - if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') { + if (DEPLOY_TOOL_NAMES.has(name) && isSuccess) { + const out = output as Record | undefined + const deployedWorkflowId = (out?.workflowId as string) ?? undefined + if (deployedWorkflowId && typeof out?.isDeployed === 'boolean') { deps.queryClient.invalidateQueries({ queryKey: deploymentKeys.info(deployedWorkflowId) }) deps.queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(deployedWorkflowId) }) deps.queryClient.invalidateQueries({ queryKey: workflowKeys.list(deps.workspaceId) }) } } - if (FOLDER_TOOL_NAMES.has(tc.name) && tc.status === 'success') { + if (FOLDER_TOOL_NAMES.has(name) && isSuccess) { deps.queryClient.invalidateQueries({ queryKey: folderKeys.list(deps.workspaceId) }) } - if (WORKFLOW_MUTATION_TOOL_NAMES.has(tc.name) && tc.status === 'success') { + if (WORKFLOW_MUTATION_TOOL_NAMES.has(name) && isSuccess) { deps.queryClient.invalidateQueries({ queryKey: workflowKeys.list(deps.workspaceId) }) } const extractedResources = - tc.status === 'success' && isResourceToolName(tc.name) - ? extractResourcesFromToolResult( - tc.name, - state.toolArgsMap.get(id) as Record | undefined, - tc.result?.output - ) + isSuccess && isResourceToolName(name) + ? extractResourcesFromToolResult(name, params, output) : [] - for (const resource of extractedResources) { invalidateResourceQueries(deps.queryClient, deps.workspaceId, resource.type, resource.id) } - if ((tc.name === 'edit_content' || tc.name === WorkspaceFile.id) && tc.status === 'success') { - const editOutput = tc.result?.output as Record | undefined + if ((name === 'edit_content' || name === WorkspaceFile.id) && isSuccess) { + const out = output as Record | undefined const editData = - editOutput && typeof editOutput.data === 'object' && editOutput.data !== null - ? (editOutput.data as Record) + out && typeof out.data === 'object' && out.data !== null + ? (out.data as Record) : undefined const editedFileId = (typeof editData?.id === 'string' ? editData.id : undefined) ?? @@ -135,162 +111,81 @@ function applyToolResult( } } - deps.onToolResultRef.current?.(tc.name, tc.status === 'success', tc.result?.output) + deps.onToolResultRef.current?.(name, isSuccess, output) const workspaceFileOperation = - tc.name === WorkspaceFile.id && typeof tc.params?.operation === 'string' - ? tc.params.operation + name === WorkspaceFile.id && typeof params?.operation === 'string' + ? params.operation : undefined const shouldKeepWorkspacePreviewOpen = - tc.name === WorkspaceFile.id && + name === WorkspaceFile.id && (workspaceFileOperation === 'append' || workspaceFileOperation === 'update' || workspaceFileOperation === 'patch') - if ( - (tc.name === WorkspaceFile.id || tc.name === 'edit_content') && - !shouldKeepWorkspacePreviewOpen - ) { - if (tc.name === WorkspaceFile.id) { - deps.removePreviewSessionImmediate(id) + if ((name === WorkspaceFile.id || name === 'edit_content') && !shouldKeepWorkspacePreviewOpen) { + if (name === WorkspaceFile.id) { + deps.removePreviewSessionImmediate(node.id) } const fileResource = extractedResources.find((r) => r.type === 'file') if (fileResource) { deps.promoteFileResource(fileResource.id, fileResource.title) deps.setActiveResourceId(fileResource.id) invalidateResourceQueries(deps.queryClient, deps.workspaceId, 'file', fileResource.id) - } else if (tc.calledBy !== FILE_SUBAGENT_ID) { + } else if (calledBy !== FILE_SUBAGENT_ID) { deps.setResources((rs) => rs.filter((r) => r.id !== 'streaming-file')) } } } -export function handleToolEvent( - ctx: StreamLoopContext, - parsed: ToolEvent, - scope: StreamEventScope -): void { +/** + * Side effects for tool events. State (the tool node, its status, args, and the + * edit_content row merge) is owned by `reduceEvent`; this handler routes preview + * phases, fires client workflow tools, and runs result side effects, then + * flushes the model-derived snapshot. + */ +export function handleToolEvent(ctx: StreamLoopContext, parsed: ToolEvent): void { const { state, ops, deps } = ctx - const { scopedSubagent, scopedParentToolCallId, spanIdentity } = scope const payload = parsed.payload - const id = payload.toolCallId + const rawId = payload.toolCallId if ('previewPhase' in payload) { + // The file preview panel is a separate concern: forward the phase to the + // preview controller, never coupling it to tool-row status. deps.onPreviewPhase(payload, parsed.stream?.streamId) return } if (payload.phase === MothershipStreamV1ToolPhase.args_delta) { - const delta = payload.argumentsDelta - if (!delta) return - - const idx = state.toolMap.get(id) - if (idx !== undefined && state.blocks[idx].toolCall) { - const tc = state.blocks[idx].toolCall! - tc.streamingArgs = (tc.streamingArgs ?? '') + delta - const displayTitle = resolveStreamingToolDisplayTitle(tc.name, tc.streamingArgs) - if (displayTitle) tc.displayTitle = displayTitle - - ops.flush() - } + ops.flushText() return } + const node = state.model.nodes.get(resolveToolId(state.model, rawId)) + if (payload.phase === MothershipStreamV1ToolPhase.result) { - const idx = state.toolMap.get(id) - if (idx === undefined || !state.blocks[idx].toolCall) { - state.pendingToolResults.set(id, payload) - return - } - applyToolResult(ctx, idx, id, payload) + if (node?.kind === 'tool' && node.result) runToolResultSideEffects(ctx, node) + ops.flush() return } + // Call phase. If a buffered result-before-call was applied to this node by the + // reducer, run its side effects now (the result event had no node to act on). + if (node?.kind === 'tool' && node.result) runToolResultSideEffects(ctx, node) + const name = payload.toolName const isPartial = payload.partial === true || payload.status === MothershipStreamV1ToolStatus.generating - if (isToolHiddenInUi(name)) { - return - } - const ui = getToolUI(payload.ui) - if (ui?.hidden) return - let displayTitle = ui?.title - const args = payload.arguments as Record | undefined - - displayTitle = resolveToolDisplayTitle(name, args) ?? displayTitle - - if (name === 'edit_content') { - const parentToolCallId = deps.latestPreviewTargetToolCallIdRef.current - const parentIdx = parentToolCallId !== null ? state.toolMap.get(parentToolCallId) : undefined - const parentToolCall = parentIdx !== undefined ? state.blocks[parentIdx].toolCall : undefined - const parentPreviewSession = - parentToolCallId !== null ? deps.previewSessionsRef.current[parentToolCallId] : undefined - const canReuseParentRow = - parentToolCall !== undefined && - (!isTerminalToolCallStatus(parentToolCall.status) || - (parentToolCall.status === ToolCallStatus.success && - parentPreviewSession !== undefined && - parentPreviewSession.status !== 'complete')) - if (parentIdx !== undefined && parentToolCall && canReuseParentRow) { - state.toolMap.set(id, parentIdx) - parentToolCall.status = 'executing' - parentToolCall.result = undefined - ops.flush() - return - } - } - - const existingToolCall = state.toolMap.has(id) - ? state.blocks[state.toolMap.get(id)!]?.toolCall - : undefined - const isNewToolCall = !existingToolCall - if (isNewToolCall) { - ops.stampBlockEnd(state.blocks[state.blocks.length - 1]) - state.toolMap.set(id, state.blocks.length) - const parentToolCallIdForBlock = ops.resolveParentForSubagentBlock( - scopedSubagent, - scopedParentToolCallId - ) - state.blocks.push({ - type: 'tool_call', - toolCall: { - id, - name, - status: 'executing', - displayTitle, - params: args, - calledBy: scopedSubagent, - }, - ...(parentToolCallIdForBlock ? { parentToolCallId: parentToolCallIdForBlock } : {}), - ...spanIdentity, - timestamp: Date.now(), - }) - if (name === ReadTool.id || isResourceToolName(name)) { - if (args) state.toolArgsMap.set(id, args) - } - const pendingResult = state.pendingToolResults.get(id) - if (pendingResult !== undefined) { - state.pendingToolResults.delete(id) - applyToolResult(ctx, state.toolMap.get(id)!, id, pendingResult) - } - } else { - const idx = state.toolMap.get(id)! - const tc = state.blocks[idx].toolCall - if (tc) { - tc.name = name - if (displayTitle) tc.displayTitle = displayTitle - if (args) tc.params = args - } - } - ops.flush() - if (isWorkflowToolName(name) && !isPartial) { const shouldStartWorkflowTool = - !deps.options.suppressedWorkflowToolStartIds?.has(id) && - (isNewToolCall || - (existingToolCall?.status === ToolCallStatus.executing && !existingToolCall.result)) + !deps.options.suppressedWorkflowToolStartIds?.has(rawId) && + node?.kind === 'tool' && + node.status === 'running' && + !node.result if (shouldStartWorkflowTool) { - deps.startClientWorkflowTool(id, name, args ?? {}) + const args = payload.arguments as Record | undefined + deps.startClientWorkflowTool(rawId, name, args ?? {}) } } + ops.flush() } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/index.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/index.ts index e0b9c3ff895..0ec7ddf433c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/index.ts @@ -9,3 +9,4 @@ export { type StreamLoopState, } from './stream-context' export { finalizeResidualToolCalls } from './stream-helpers' +export { applyTurnTerminal } from './turn-model' diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.test.ts index 4d616426047..67eb6ab1b23 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.test.ts @@ -3,11 +3,23 @@ */ import { describe, expect, it, vi } from 'vitest' -import type { MothershipStreamV1ErrorPayload } from '@/lib/copilot/generated/mothership-stream-v1' +import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' import type { ChatMessage, ContentBlock } from '@/app/workspace/[workspaceId]/home/types' import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' import { createStreamLoopContext } from './stream-context' import { makeStreamLoopDeps, ref } from './stream-test-helpers' +import { reduceEvent } from './turn-model' + +function textEnvelope(text: string): PersistedStreamEventEnvelope { + return { + v: 1, + seq: 1, + ts: '', + stream: { streamId: 's', cursor: '1' }, + type: 'text', + payload: { channel: 'assistant', text }, + } as unknown as PersistedStreamEventEnvelope +} describe('createStreamLoopContext', () => { describe('isStale', () => { @@ -82,7 +94,7 @@ describe('createStreamLoopContext', () => { }) describe('preserveExistingState reconnect hydration', () => { - it('rebuilds blocks, toolMap, toolArgsMap, subagentBySpanId and recovers the active subagent', () => { + it('rebuilds the model (tools and subagent lanes) from the persisted snapshot', () => { const blocks: ContentBlock[] = [ { type: 'text', content: 'hi' }, { @@ -103,16 +115,15 @@ describe('createStreamLoopContext', () => { streamingContentRef: ref('hi'), }) ) - expect(ctx.state.blocks).toHaveLength(3) - expect(ctx.state.runningText).toBe('hi') - expect(ctx.state.toolMap.get('tc-1')).toBe(1) - expect(ctx.state.toolArgsMap.get('tc-1')).toEqual({ path: '/a' }) - expect(ctx.state.subagentBySpanId.get('span-1')).toBe('file') - expect(ctx.state.activeSubagent).toBe('file') - expect(ctx.state.activeSubagentParentToolCallId).toBe('tc-1') + const tool = ctx.state.model.nodes.get('tc-1') + expect(tool?.kind).toBe('tool') + expect((tool as { status: string }).status).toBe('success') + const agent = ctx.state.model.nodes.get('span-1') + expect(agent?.kind).toBe('agent') + expect((agent as { agentId: string }).agentId).toBe('file') }) - it('stops recovering the active subagent at a subagent_end marker', () => { + it('rebuilds a closed subagent lane as terminal at a subagent_end marker', () => { const blocks: ContentBlock[] = [ { type: 'subagent', content: 'file', spanId: 'span-1' }, { type: 'subagent_end', spanId: 'span-1' }, @@ -123,7 +134,9 @@ describe('createStreamLoopContext', () => { streamingBlocksRef: ref(blocks), }) ) - expect(ctx.state.activeSubagent).toBeUndefined() + const agent = ctx.state.model.nodes.get('span-1') + expect(agent?.kind).toBe('agent') + expect((agent as { status: string }).status).not.toBe('running') }) it('does not clear the shared refs on a preserve-state stream', () => { @@ -146,7 +159,6 @@ describe('createStreamLoopContext', () => { const ctx = createStreamLoopContext( makeStreamLoopDeps({ expectedGen: 1, streamGenRef: ref(2), setPendingMessages }) ) - ctx.state.blocks.push({ type: 'text', content: 'x' }) ctx.ops.flush() expect(setPendingMessages).not.toHaveBeenCalled() }) @@ -156,8 +168,8 @@ describe('createStreamLoopContext', () => { const ctx = createStreamLoopContext( makeStreamLoopDeps({ chatIdRef: ref(undefined), setPendingMessages }) ) - ctx.state.runningText = 'hello' - ctx.state.blocks.push({ type: 'text', content: 'hello' }) + // flush serializes the model (the single source of truth) into the snapshot. + reduceEvent(ctx.state.model, textEnvelope('hello')) ctx.ops.flush() expect(setPendingMessages).toHaveBeenCalledTimes(1) const updater = setPendingMessages.mock.calls[0][0] as (prev: ChatMessage[]) => ChatMessage[] @@ -190,61 +202,4 @@ describe('createStreamLoopContext', () => { expect(setPendingMessages).not.toHaveBeenCalled() }) }) - - describe('block builders', () => { - it('ensureTextBlock coalesces consecutive same-scope text blocks', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - const a = ctx.ops.ensureTextBlock(undefined, undefined, undefined, {}) - const b = ctx.ops.ensureTextBlock(undefined, undefined, undefined, {}) - expect(a).toBe(b) - expect(ctx.state.blocks).toHaveLength(1) - }) - - it('ensureTextBlock starts a new block on a subagent-scope change and stamps the prior end', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - const main = ctx.ops.ensureTextBlock(undefined, undefined, undefined, {}) - const sub = ctx.ops.ensureTextBlock('file', undefined, undefined, { spanId: 's1' }) - expect(sub).not.toBe(main) - expect(ctx.state.blocks).toHaveLength(2) - expect(main.endedAt).toBeTypeOf('number') - expect(sub.spanId).toBe('s1') - expect(sub.subagent).toBe('file') - }) - - it('ensureThinkingBlock uses subagent_thinking under a subagent', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - const tb = ctx.ops.ensureThinkingBlock('file', 'tc', undefined, {}) - expect(tb.type).toBe('subagent_thinking') - }) - - it('toEventMs falls back to a finite now on an invalid timestamp', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - const ms = ctx.ops.toEventMs('not-a-date') - expect(Number.isFinite(ms)).toBe(true) - }) - - it('resolveScopedSubagent prefers agentId, then spanId, then parentToolCallId, then active', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - ctx.state.subagentBySpanId.set('s1', 'spanAgent') - ctx.state.subagentByParentToolCallId.set('p1', 'parentAgent') - ctx.state.activeSubagent = 'activeAgent' - expect(ctx.ops.resolveScopedSubagent('explicit', 'p1', 's1')).toBe('explicit') - expect(ctx.ops.resolveScopedSubagent(undefined, 'p1', 's1')).toBe('spanAgent') - expect(ctx.ops.resolveScopedSubagent(undefined, 'p1', undefined)).toBe('parentAgent') - expect(ctx.ops.resolveScopedSubagent(undefined, undefined, undefined)).toBe('activeAgent') - }) - - it('buildInlineErrorTag includes the message, code and provider', () => { - const ctx = createStreamLoopContext(makeStreamLoopDeps()) - const tag = ctx.ops.buildInlineErrorTag({ - message: 'boom', - code: 'E1', - provider: 'openai', - } as unknown as MothershipStreamV1ErrorPayload) - expect(tag).toContain('mothership-error') - expect(tag).toContain('boom') - expect(tag).toContain('E1') - expect(tag).toContain('openai') - }) - }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts index 07d998bfe20..b49871bfbdf 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts @@ -3,10 +3,17 @@ import type { QueryClient } from '@tanstack/react-query' import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' import type { RevealedSimKeysByMessage } from '@/lib/copilot/chat/sim-key-redaction' import { captureRevealedSimKeys } from '@/lib/copilot/chat/sim-key-redaction' -import type { MothershipStreamV1ErrorPayload } from '@/lib/copilot/generated/mothership-stream-v1' import type { SyntheticFilePreviewPayload } from '@/lib/copilot/request/session' import type { FilePreviewSession } from '@/lib/copilot/request/session/file-preview-session-contract' -import type { ToolResultPhasePayload } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers' +import { + createTurnModel, + type TurnModel, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model' +import { + contentBlocksToModel, + modelMainText, + modelToContentBlocks, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize' import type { ChatMessage, ContentBlock, @@ -30,34 +37,24 @@ export interface StreamLoopOptions { } export interface StreamLoopState { - blocks: ContentBlock[] - toolMap: Map - toolArgsMap: Map> - subagentByParentToolCallId: Map - subagentBySpanId: Map - pendingToolResults: Map - runningText: string - lastContentSource: 'main' | 'subagent' | null + /** + * The normalized turn model — the single source of truth for streamed state. + * `reduceEvent` folds every event into it; `flush` serializes it to the + * persisted/rendered `contentBlocks` shape. The handlers carry no block state. + */ + model: TurnModel streamRequestId: string | undefined - activeSubagent: string | undefined - activeSubagentParentToolCallId: string | undefined - activeCompactionId: string | undefined sawStreamError: boolean sawCompleteEvent: boolean scheduledTextFlushFrame: number | null } export interface StreamEventScope { - scopedSubagent: string | undefined scopedParentToolCallId: string | undefined scopedAgentId: string | undefined scopedSpanId: string | undefined - scopedParentSpanId: string | undefined - spanIdentity: { spanId?: string; parentSpanId?: string } } -type SpanIdentity = { spanId?: string; parentSpanId?: string } - export interface StreamLoopDeps { workspaceId: string queryClient: QueryClient @@ -136,36 +133,6 @@ export interface StreamLoopDeps { export interface StreamLoopOps { isStale: () => boolean - toEventMs: (ts: string | undefined) => number - stampBlockEnd: (block: ContentBlock | undefined, ts?: string) => void - ensureTextBlock: ( - subagentName: string | undefined, - parentToolCallId: string | undefined, - ts?: string, - identity?: SpanIdentity - ) => ContentBlock - ensureThinkingBlock: ( - subagentName: string | undefined, - parentToolCallId: string | undefined, - ts?: string, - identity?: SpanIdentity - ) => ContentBlock - resolveScopedSubagent: ( - agentId: string | undefined, - parentToolCallId: string | undefined, - spanId?: string - ) => string | undefined - resolveParentForSubagentBlock: ( - subagent: string | undefined, - scopedParent: string | undefined - ) => string | undefined - appendInlineErrorTag: ( - tag: string, - subagentName?: string, - parentToolCallId?: string, - ts?: string - ) => void - buildInlineErrorTag: (payload: MothershipStreamV1ErrorPayload) => string flush: () => void flushText: () => void } @@ -189,18 +156,12 @@ export function createStreamLoopContext(deps: StreamLoopDeps): StreamLoopContext const preserveState = deps.options.preserveExistingState === true const state: StreamLoopState = { - blocks: preserveState ? [...deps.streamingBlocksRef.current] : [], - toolMap: new Map(), - toolArgsMap: new Map>(), - subagentByParentToolCallId: new Map(), - subagentBySpanId: new Map(), - pendingToolResults: new Map(), - runningText: preserveState ? deps.streamingContentRef.current || '' : '', - lastContentSource: null, + // On a reconnect that preserves state, rebuild the model from the last + // serialized snapshot so live events fold into the identical model. + model: preserveState + ? contentBlocksToModel(deps.streamingBlocksRef.current) + : createTurnModel(), streamRequestId: undefined, - activeSubagent: undefined, - activeSubagentParentToolCallId: undefined, - activeCompactionId: undefined, sawStreamError: false, sawCompleteEvent: false, scheduledTextFlushFrame: null, @@ -210,140 +171,29 @@ export function createStreamLoopContext(deps: StreamLoopDeps): StreamLoopContext (deps.expectedGen !== undefined && deps.streamGenRef.current !== deps.expectedGen) || deps.options.shouldContinue?.() === false - if (preserveState) { - for (let i = 0; i < state.blocks.length; i++) { - const tc = state.blocks[i].toolCall - if (tc) { - state.toolMap.set(tc.id, i) - if (tc.params) state.toolArgsMap.set(tc.id, tc.params) - } - } - for (const block of state.blocks) { - if (block.type === 'subagent' && block.spanId && block.content) { - state.subagentBySpanId.set(block.spanId, block.content) - } - } - for (let i = state.blocks.length - 1; i >= 0; i--) { - if (state.blocks[i].type === 'subagent' && state.blocks[i].content) { - state.activeSubagent = state.blocks[i].content - state.activeSubagentParentToolCallId = state.blocks[i].parentToolCallId - break - } - if (state.blocks[i].type === 'subagent_end') { - break - } - } - } else if (!isStale()) { + if (!preserveState && !isStale()) { deps.streamingContentRef.current = '' deps.streamingBlocksRef.current = [] } - const toEventMs = (ts: string | undefined): number => { - if (ts) { - const parsed = Date.parse(ts) - if (Number.isFinite(parsed)) return parsed - } - return Date.now() - } - - const stampBlockEnd = (block: ContentBlock | undefined, ts?: string) => { - if (block && block.endedAt === undefined) block.endedAt = toEventMs(ts) - } - - const ensureTextBlock = ( - subagentName: string | undefined, - parentToolCallId: string | undefined, - ts?: string, - identity?: SpanIdentity - ): ContentBlock => { - const last = state.blocks[state.blocks.length - 1] - if ( - last?.type === 'text' && - last.subagent === subagentName && - last.parentToolCallId === parentToolCallId && - last.spanId === identity?.spanId - ) { - return last - } - stampBlockEnd(last, ts) - const b: ContentBlock = { type: 'text', content: '', timestamp: toEventMs(ts) } - if (subagentName) b.subagent = subagentName - if (parentToolCallId) b.parentToolCallId = parentToolCallId - if (identity?.spanId) b.spanId = identity.spanId - if (identity?.parentSpanId) b.parentSpanId = identity.parentSpanId - state.blocks.push(b) - return b - } - - const ensureThinkingBlock = ( - subagentName: string | undefined, - parentToolCallId: string | undefined, - ts?: string, - identity?: SpanIdentity - ): ContentBlock => { - const targetType = subagentName ? 'subagent_thinking' : 'thinking' - const last = state.blocks[state.blocks.length - 1] - if ( - last?.type === targetType && - last.subagent === subagentName && - last.parentToolCallId === parentToolCallId && - last.spanId === identity?.spanId - ) { - return last - } - stampBlockEnd(last, ts) - const b: ContentBlock = { type: targetType, content: '', timestamp: toEventMs(ts) } - if (subagentName) b.subagent = subagentName - if (parentToolCallId) b.parentToolCallId = parentToolCallId - if (identity?.spanId) b.spanId = identity.spanId - if (identity?.parentSpanId) b.parentSpanId = identity.parentSpanId - state.blocks.push(b) - return b - } - - const resolveScopedSubagent = ( - agentId: string | undefined, - parentToolCallId: string | undefined, - spanId?: string - ): string | undefined => { - if (agentId) return agentId - if (spanId) { - const scoped = state.subagentBySpanId.get(spanId) - if (scoped) return scoped - } - if (parentToolCallId) { - const scoped = state.subagentByParentToolCallId.get(parentToolCallId) - if (scoped) return scoped - } - return state.activeSubagent - } - - const resolveParentForSubagentBlock = ( - subagent: string | undefined, - scopedParent: string | undefined - ): string | undefined => { - if (!subagent) return undefined - if (scopedParent) return scopedParent - if (state.activeSubagent === subagent) return state.activeSubagentParentToolCallId - for (const [parent, name] of state.subagentByParentToolCallId) { - if (name === subagent) return parent - } - return undefined - } - const flush = () => { if (isStale()) return - deps.streamingBlocksRef.current = [...state.blocks] + // The model is authoritative: serialize it to the persisted/rendered block + // shape and main-lane content for every snapshot write. + const modelBlocks = modelToContentBlocks(state.model) + const modelContent = modelMainText(state.model) + deps.streamingBlocksRef.current = modelBlocks + deps.streamingContentRef.current = modelContent captureRevealedSimKeys( deps.revealedSimKeysRef.current, [deps.assistantId, state.streamRequestId], - state.runningText + modelContent ) const activeChatId = deps.options.targetChatId ?? deps.chatIdRef.current if (!activeChatId) { const snapshot: Partial = { - content: state.runningText, - contentBlocks: [...state.blocks], + content: modelContent, + contentBlocks: modelBlocks, } if (state.streamRequestId) snapshot.requestId = state.streamRequestId deps.setPendingMessages((prev) => { @@ -364,8 +214,8 @@ export function createStreamLoopContext(deps: StreamLoopDeps): StreamLoopContext const assistantMessage = deps.buildAssistantSnapshotMessage({ id: deps.assistantId, - content: state.runningText, - contentBlocks: state.blocks, + content: modelContent, + contentBlocks: modelBlocks, ...(state.streamRequestId ? { requestId: state.streamRequestId } : {}), }) deps.upsertMothershipChatHistory(activeChatId, (current) => { @@ -404,46 +254,8 @@ export function createStreamLoopContext(deps: StreamLoopDeps): StreamLoopContext }) } - const appendInlineErrorTag = ( - tag: string, - subagentName?: string, - parentToolCallId?: string, - ts?: string - ) => { - if (state.runningText.includes(tag)) return - const tb = ensureTextBlock(subagentName, parentToolCallId, ts) - const prefix = state.runningText.length > 0 && !state.runningText.endsWith('\n') ? '\n' : '' - tb.content = `${tb.content ?? ''}${prefix}${tag}` - state.runningText += `${prefix}${tag}` - deps.streamingContentRef.current = state.runningText - flush() - } - - const buildInlineErrorTag = (payload: MothershipStreamV1ErrorPayload) => { - const message = - (typeof payload.displayMessage === 'string' ? payload.displayMessage : undefined) || - (typeof payload.message === 'string' ? payload.message : undefined) || - (typeof payload.error === 'string' ? payload.error : undefined) || - 'An unexpected error occurred' - const provider = typeof payload.provider === 'string' ? payload.provider : undefined - const code = typeof payload.code === 'string' ? payload.code : undefined - return `${JSON.stringify({ - message, - ...(code ? { code } : {}), - ...(provider ? { provider } : {}), - })}` - } - const ops: StreamLoopOps = { isStale, - toEventMs, - stampBlockEnd, - ensureTextBlock, - ensureThinkingBlock, - resolveScopedSubagent, - resolveParentForSubagentBlock, - appendInlineErrorTag, - buildInlineErrorTag, flush, flushText, } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index 9209614ec4f..9560560e1b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -1,7 +1,5 @@ import { createLogger } from '@sim/logger' import { isRecordLike } from '@sim/utils/object' -import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome' -import type { MothershipStreamV1ToolUI } from '@/lib/copilot/generated/mothership-stream-v1' import { CrawlWebsite, CreateFolder, @@ -69,61 +67,43 @@ export const WORKFLOW_MUTATION_TOOL_NAMES: Set = new Set([ export type StreamPayload = Record -export type StreamToolUI = { - hidden?: boolean - title?: string - clientExecutable?: boolean -} - -export type ToolResultPhasePayload = { - output?: unknown - status?: string - error?: unknown - success?: boolean -} - export function asPayloadRecord(value: unknown): StreamPayload | undefined { return isRecordLike(value) ? value : undefined } -export function getToolUI(ui?: MothershipStreamV1ToolUI): StreamToolUI | undefined { - if (!ui) { - return undefined - } - - const title = - typeof ui.title === 'string' - ? ui.title - : typeof ui.phaseLabel === 'string' - ? ui.phaseLabel - : undefined - - return { - ...(typeof ui.hidden === 'boolean' ? { hidden: ui.hidden } : {}), - ...(title ? { title } : {}), - ...(typeof ui.clientExecutable === 'boolean' ? { clientExecutable: ui.clientExecutable } : {}), - } -} - +/** + * Settles any tool row still `executing` at a turn terminal by propagating the + * turn's outcome — the deterministic replacement for the old `interrupted` + * invention. A clean `complete` means the turn succeeded, so a straggler is + * settled `success` (with explicit tool/span terminals from the backend there + * are normally none); a stop settles `cancelled`; an error settles `error`. + */ export function finalizeResidualToolCalls( blocks: ContentBlock[], turnTerminal: 'complete' | 'cancelled' | 'error' ): void { const endedAt = Date.now() + const propagated = + turnTerminal === 'cancelled' + ? ToolCallStatus.cancelled + : turnTerminal === 'error' + ? ToolCallStatus.error + : ToolCallStatus.success for (const block of blocks) { + // Close any still-open subagent lane at the turn terminal so its group + // resolves deterministically even when the backend cut off before a + // `span end` (abort/disconnect). The projection treats a stamped `endedAt` + // as a closed group, so the delegating spinner clears without any + // transport-based gating. + if (block.type === 'subagent' && block.endedAt === undefined) { + block.endedAt = endedAt + continue + } const tc = block.toolCall if (!tc || tc.status !== ToolCallStatus.executing) continue - if (turnTerminal === 'cancelled') { - tc.status = ToolCallStatus.cancelled + tc.status = propagated + if (propagated === ToolCallStatus.cancelled) { tc.displayTitle = 'Stopped by user' - } else if (turnTerminal === 'error') { - tc.status = ToolCallStatus.error - } else { - tc.status = ToolCallStatus.interrupted - logger.warn('Tool call unresolved at turn completion', { - toolCallId: tc.id, - toolName: tc.name, - }) } if (block.endedAt === undefined) { block.endedAt = endedAt @@ -131,27 +111,6 @@ export function finalizeResidualToolCalls( } } -export function isTerminalToolCallStatus(status: ToolCallStatus): boolean { - return ( - status === ToolCallStatus.success || - status === ToolCallStatus.error || - status === ToolCallStatus.cancelled || - status === ToolCallStatus.skipped || - status === ToolCallStatus.rejected || - status === ToolCallStatus.interrupted - ) -} - -export function resolveLiveToolStatus( - payload: Partial<{ - status: string - success: boolean - output: unknown - }> -): ToolCallStatus { - return resolveStreamToolOutcome(payload) as ToolCallStatus -} - function resolveLeafWorkflowPathSegment(segments: string[]): string | undefined { const lastSegment = segments[segments.length - 1] if (!lastSegment) return undefined diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts new file mode 100644 index 00000000000..1690cd080e8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts @@ -0,0 +1,408 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' +import { + type AgentNode, + applyTurnTerminal, + createTurnModel, + reduceEvent, + type ToolNode, + type TurnModel, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model' +import { + contentBlocksToModel, + modelToContentBlocks, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize' + +interface Scope { + lane: 'subagent' + spanId?: string + parentSpanId?: string + parentToolCallId?: string + agentId?: string +} + +function env(seq: number, type: string, payload: Record, scope?: Scope) { + return { + v: 1, + seq, + // Real ts so tsMs === seq, exercising the wall-clock timing path. + ts: new Date(seq).toISOString(), + stream: { streamId: 's1', cursor: String(seq) }, + type, + payload, + ...(scope ? { scope } : {}), + } as unknown as PersistedStreamEventEnvelope +} + +function build(events: PersistedStreamEventEnvelope[]): TurnModel { + const m = createTurnModel() + for (const e of events) reduceEvent(m, e) + return m +} + +// A main-agent file delegation: trigger tool (main lane), subagent span, inner +// workspace_file, span end, delegation result. +function fileDelegationEvents(): PersistedStreamEventEnvelope[] { + const sub: Scope = { + lane: 'subagent', + spanId: 'S1', + parentSpanId: 'main', + parentToolCallId: 'tc-file', + agentId: 'file', + } + return [ + env(1, 'text', { channel: 'assistant', text: 'Writing the file.' }), + env(2, 'tool', { phase: 'call', toolCallId: 'tc-file', toolName: 'file' }), + env( + 3, + 'span', + { kind: 'subagent', event: 'start', agent: 'file', data: { tool_call_id: 'tc-file' } }, + sub + ), + env( + 4, + 'tool', + { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { lane: 'subagent', spanId: 'S1' } + ), + env( + 5, + 'tool', + { phase: 'result', toolCallId: 'wf-1', toolName: 'workspace_file', success: true }, + { lane: 'subagent', spanId: 'S1' } + ), + env( + 6, + 'span', + { kind: 'subagent', event: 'end', agent: 'file', data: {} }, + { lane: 'subagent', spanId: 'S1' } + ), + env(7, 'tool', { phase: 'result', toolCallId: 'tc-file', toolName: 'file', success: true }), + ] +} + +function blocksByType(blocks: ReturnType, type: string) { + return blocks.filter((b) => b.type === type) +} + +describe('modelToContentBlocks', () => { + it('emits main-lane blocks without spanId and subagent-lane blocks with spanId', () => { + const blocks = modelToContentBlocks(build(fileDelegationEvents())) + + const mainText = blocks.find((b) => b.type === 'text') + expect(mainText?.spanId).toBeUndefined() + + const trigger = blocksByType(blocks, 'tool_call').find((b) => b.toolCall?.name === 'file') + expect(trigger?.spanId).toBeUndefined() + expect(trigger?.toolCall?.status).toBe('success') + + const innerTool = blocksByType(blocks, 'tool_call').find( + (b) => b.toolCall?.name === 'workspace_file' + ) + expect(innerTool?.spanId).toBe('S1') + expect(innerTool?.toolCall?.calledBy).toBe('file') + expect(innerTool?.toolCall?.status).toBe('success') + + const subagent = blocks.find((b) => b.type === 'subagent') + expect(subagent?.spanId).toBe('S1') + expect(subagent?.parentSpanId).toBe('main') + expect(subagent?.parentToolCallId).toBe('tc-file') + }) + + it('orders blocks by wire seq and appends new content without reordering existing blocks', () => { + const m = createTurnModel() + reduceEvent(m, env(1, 'text', { channel: 'assistant', text: 'one' })) + reduceEvent(m, env(2, 'tool', { phase: 'call', toolCallId: 't1', toolName: 'search' })) + const snap1 = modelToContentBlocks(m) + expect(snap1.map((b) => b.type)).toEqual(['text', 'tool_call']) + + // Later events arrive; the tool settles and new text starts. + reduceEvent( + m, + env(3, 'tool', { phase: 'result', toolCallId: 't1', toolName: 'search', success: true }) + ) + reduceEvent(m, env(4, 'text', { channel: 'assistant', text: 'two' })) + const snap2 = modelToContentBlocks(m) + + // Existing blocks keep their position (snap1 is a prefix of snap2); new text appends. + expect(snap2.map((b) => b.type)).toEqual(['text', 'tool_call', 'text']) + expect(snap2[1].toolCall?.id).toBe('t1') + expect(snap2[0].content).toBe('one') + }) + + it('attributes subagent content that streams before its subagent_start (parallel-burst inversion)', () => { + const sub: Scope = { + lane: 'subagent', + spanId: 'R1', + parentSpanId: 'main', + parentToolCallId: 'tc-r1', + agentId: 'research', + } + const m = createTurnModel() + reduceEvent(m, env(1, 'text', { channel: 'assistant', text: 'Spawning research.' })) + // Under an 8-way burst the subagent's thinking + text can be reduced before + // its subagent_start lands. The content already carries the lane identity. + reduceEvent(m, env(2, 'text', { channel: 'thinking', text: 'Considering odds.' }, sub)) + reduceEvent(m, env(3, 'text', { channel: 'assistant', text: 'Team analysis.' }, sub)) + + // Snapshot mid-burst (before the start): the research content must already be + // its own lane, never leaked into the main ("Sim") lane with its thinking dropped. + const mid = modelToContentBlocks(m) + const midSub = mid.find((b) => b.type === 'subagent') + expect(midSub?.content).toBe('research') + expect(midSub?.spanId).toBe('R1') + expect(mid.find((b) => b.type === 'subagent_thinking')?.spanId).toBe('R1') + expect(mid.filter((b) => b.type === 'text' && b.spanId === 'R1')).toHaveLength(1) + // The main lane holds only the pre-spawn text — nothing leaked in. + const mainText = mid.filter((b) => b.type === 'text' && !b.spanId) + expect(mainText).toHaveLength(1) + expect(mainText[0].content).toBe('Spawning research.') + + // The real subagent_start lands afterward and no-ops: still one research lane. + reduceEvent( + m, + env( + 4, + 'span', + { kind: 'subagent', event: 'start', agent: 'research', data: { tool_call_id: 'tc-r1' } }, + sub + ) + ) + const after = modelToContentBlocks(m) + expect(after.filter((b) => b.type === 'subagent')).toHaveLength(1) + expect(after.find((b) => b.type === 'subagent')?.content).toBe('research') + }) + + it('places subagent_end at its end seq (after the lane work), never reordering siblings', () => { + const sub: Scope = { + lane: 'subagent', + spanId: 'S1', + parentToolCallId: 'tc-file', + agentId: 'file', + } + const blocks = modelToContentBlocks( + build([ + env(1, 'text', { channel: 'assistant', text: 'before' }), + env(2, 'tool', { phase: 'call', toolCallId: 'tc-file', toolName: 'file' }), + env( + 3, + 'span', + { kind: 'subagent', event: 'start', agent: 'file', data: { tool_call_id: 'tc-file' } }, + sub + ), + env( + 4, + 'tool', + { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { lane: 'subagent', spanId: 'S1' } + ), + env( + 5, + 'span', + { kind: 'subagent', event: 'end', agent: 'file', data: {} }, + { lane: 'subagent', spanId: 'S1' } + ), + env(6, 'text', { channel: 'assistant', text: 'after' }), + ]) + ) + const types = blocks.map((b) => b.type) + const innerIdx = blocks.findIndex((b) => b.toolCall?.name === 'workspace_file') + const endIdx = types.indexOf('subagent_end') + const afterIdx = blocks.findIndex((b) => b.type === 'text' && b.content === 'after') + // subagent_end sits after the inner work and before the trailing main text — no sibling jumps. + expect(endIdx).toBeGreaterThan(innerIdx) + expect(afterIdx).toBeGreaterThan(endIdx) + }) + + it('preserves thinking timing across a model -> blocks -> model reconnect round-trip', () => { + const m1 = build([ + env(1, 'text', { channel: 'thinking', text: 'pondering' }), + env(2, 'text', { channel: 'assistant', text: 'the answer' }), + ]) + const blocks1 = modelToContentBlocks(m1) + const blocks2 = modelToContentBlocks(contentBlocksToModel(blocks1)) + const t1 = blocks1.find((b) => b.type === 'thinking') + const t2 = blocks2.find((b) => b.type === 'thinking') + expect(t1?.timestamp).toBe(1) + expect(t1?.endedAt).toBe(2) + // Reconnect rebuild must not reset timing to seq/undefined. + expect(t2?.timestamp).toBe(t1?.timestamp) + expect(t2?.endedAt).toBe(t1?.endedAt) + }) + + it('emits subagent_end for a straggler lane closed by a model terminal (no span end)', () => { + const sub: Scope = { + lane: 'subagent', + spanId: 'S1', + parentToolCallId: 'tc-file', + agentId: 'file', + } + const m = build([ + env(1, 'tool', { phase: 'call', toolCallId: 'tc-file', toolName: 'file' }), + env( + 2, + 'span', + { kind: 'subagent', event: 'start', agent: 'file', data: { tool_call_id: 'tc-file' } }, + sub + ), + env( + 3, + 'tool', + { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { lane: 'subagent', spanId: 'S1' } + ), + ]) + applyTurnTerminal(m, 'error') + const blocks = modelToContentBlocks(m) + expect(blocks.some((b) => b.type === 'subagent_end' && b.spanId === 'S1')).toBe(true) + }) + + it('skips per-call hidden tool nodes but keeps them in the model for side effects', () => { + const m = build([ + env(1, 'tool', { + phase: 'call', + toolCallId: 'h-1', + toolName: 'secret_tool', + ui: { hidden: true }, + }), + env(2, 'tool', { + phase: 'result', + toolCallId: 'h-1', + toolName: 'secret_tool', + success: true, + }), + ]) + expect(m.nodes.has('h-1')).toBe(true) + expect(blocksByType(modelToContentBlocks(m), 'tool_call')).toHaveLength(0) + }) + + it('resolves a tool display title from its arguments', () => { + const blocks = modelToContentBlocks( + build([ + env(1, 'tool', { + phase: 'call', + toolCallId: 'wf', + toolName: 'workspace_file', + arguments: { operation: 'create', title: 'My Doc' }, + }), + ]) + ) + const tool = blocksByType(blocks, 'tool_call').find((b) => b.toolCall?.id === 'wf') + expect(tool?.toolCall?.displayTitle).toBeTruthy() + }) + + it('emits a paired subagent_end at the run end seq, ordered after the inner work', () => { + const blocks = modelToContentBlocks(build(fileDelegationEvents())) + const startIdx = blocks.findIndex((b) => b.type === 'subagent') + const innerIdx = blocks.findIndex( + (b) => b.type === 'tool_call' && b.toolCall?.name === 'workspace_file' + ) + const endIdx = blocks.findIndex((b) => b.type === 'subagent_end') + expect(startIdx).toBeGreaterThanOrEqual(0) + expect(endIdx).toBeGreaterThan(innerIdx) + expect(innerIdx).toBeGreaterThan(startIdx) + }) + + it('omits subagent_end while the run is still open', () => { + const sub: Scope = { + lane: 'subagent', + spanId: 'S1', + parentSpanId: 'main', + parentToolCallId: 'tc-file', + agentId: 'file', + } + const blocks = modelToContentBlocks( + build([ + env(1, 'tool', { phase: 'call', toolCallId: 'tc-file', toolName: 'file' }), + env(2, 'span', { kind: 'subagent', event: 'start', agent: 'file', data: {} }, sub), + ]) + ) + expect(blocksByType(blocks, 'subagent_end')).toHaveLength(0) + expect(blocksByType(blocks, 'subagent')).toHaveLength(1) + }) +}) + +describe('contentBlocksToModel round-trip', () => { + function tool(model: TurnModel, id: string): ToolNode { + return model.nodes.get(id) as ToolNode + } + function agent(model: TurnModel, spanId: string): AgentNode { + return model.nodes.get(spanId) as AgentNode + } + + it('rebuilds tool and agent statuses and nesting from serialized blocks', () => { + const original = build(fileDelegationEvents()) + const rebuilt = contentBlocksToModel(modelToContentBlocks(original)) + + expect(tool(rebuilt, 'tc-file').status).toBe('success') + expect(tool(rebuilt, 'wf-1').status).toBe('success') + expect(tool(rebuilt, 'wf-1').spanId).toBe('S1') + expect(agent(rebuilt, 'S1').status).toBe('success') + expect(agent(rebuilt, 'S1').parentSpanId).toBe('main') + expect(agent(rebuilt, 'S1').triggerToolCallId).toBe('tc-file') + }) + + it('preserves a running tool and an open subagent across the round-trip', () => { + const sub: Scope = { + lane: 'subagent', + spanId: 'S1', + parentSpanId: 'main', + parentToolCallId: 'tc-file', + agentId: 'file', + } + const original = build([ + env(1, 'tool', { phase: 'call', toolCallId: 'tc-file', toolName: 'file' }), + env(2, 'span', { kind: 'subagent', event: 'start', agent: 'file', data: {} }, sub), + env( + 3, + 'tool', + { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { lane: 'subagent', spanId: 'S1' } + ), + ]) + const rebuilt = contentBlocksToModel(modelToContentBlocks(original)) + expect(tool(rebuilt, 'wf-1').status).toBe('running') + expect(agent(rebuilt, 'S1').status).toBe('running') + }) + + it('round-trips parallel same-name subagents on distinct spans', () => { + const subA: Scope = { + lane: 'subagent', + spanId: 'SA', + parentSpanId: 'main', + parentToolCallId: 'tc-a', + agentId: 'file', + } + const subB: Scope = { + lane: 'subagent', + spanId: 'SB', + parentSpanId: 'main', + parentToolCallId: 'tc-b', + agentId: 'file', + } + const original = build([ + env(1, 'span', { kind: 'subagent', event: 'start', agent: 'file', data: {} }, subA), + env(2, 'span', { kind: 'subagent', event: 'start', agent: 'file', data: {} }, subB), + env( + 3, + 'span', + { kind: 'subagent', event: 'end', agent: 'file', data: {} }, + { lane: 'subagent', spanId: 'SA' } + ), + env( + 4, + 'span', + { kind: 'subagent', event: 'end', agent: 'file', data: {} }, + { lane: 'subagent', spanId: 'SB' } + ), + ]) + const rebuilt = contentBlocksToModel(modelToContentBlocks(original)) + expect(agent(rebuilt, 'SA').triggerToolCallId).toBe('tc-a') + expect(agent(rebuilt, 'SB').triggerToolCallId).toBe('tc-b') + expect(agent(rebuilt, 'SA').status).toBe('success') + expect(agent(rebuilt, 'SB').status).toBe('success') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.ts new file mode 100644 index 00000000000..3e3b6cc9f6e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.ts @@ -0,0 +1,337 @@ +import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' +import { + resolveStreamingToolDisplayTitle, + resolveToolDisplayTitle, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers' +import { + type AgentNode, + createTurnModel, + MAIN_SPAN, + type NodeStatus, + reduceEvent, + type ToolNode, + type TurnModel, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model' +import type { ContentBlock } from '@/app/workspace/[workspaceId]/home/types' +import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' + +/** + * Serialization bridge between the normalized {@link TurnModel} (the streaming + * source of truth) and the persisted/rendered `ContentBlock[]` shape. The model + * is authoritative during streaming; `flush` serializes it to blocks for the + * React-Query/pending snapshot and the DB, and the renderer keeps projecting + * blocks via the existing `parseBlocks`. `contentBlocksToModel` rebuilds the + * model from a persisted snapshot so a reconnect mid-stream continues into the + * exact same model. + */ + +function nodeToToolStatus(status: NodeStatus): ToolCallStatus { + if (status === 'running') return ToolCallStatus.executing + return status +} + +function toolStatusToNode(status: ToolCallStatus): NodeStatus { + if (status === ToolCallStatus.executing) return 'running' + if (status === ToolCallStatus.interrupted) return 'error' + return status +} + +/** + * Resolves a tool row's display title with the same precedence the live handler + * used: the streaming-args title wins while args stream, then the arg-derived + * title, then the explicit `ui.title`. + */ +function toolDisplayTitle(node: ToolNode): string | undefined { + const streamingTitle = node.streamingArgs + ? resolveStreamingToolDisplayTitle(node.name, node.streamingArgs) + : undefined + return streamingTitle ?? resolveToolDisplayTitle(node.name, node.args) ?? node.uiTitle +} + +interface SeqBlock { + seq: number + block: ContentBlock +} + +/** + * Serializes the model to ordered content blocks matching the live handler + * shapes: main-lane blocks carry no `spanId`; subagent-lane blocks carry + * `spanId`/`parentSpanId` (and `subagent` name for text). A terminated agent + * emits a paired `subagent_end` at its end seq so the projection closes the lane + * exactly as the live browser path did. + */ +export function modelToContentBlocks(model: TurnModel): ContentBlock[] { + const entries: SeqBlock[] = [] + + for (const id of model.order) { + const node = model.nodes.get(id) + if (!node) continue + const isSub = node.spanId !== MAIN_SPAN + const ownerAgent = isSub ? (model.nodes.get(node.spanId) as AgentNode | undefined) : undefined + const spanFields = isSub + ? { + spanId: node.spanId, + ...(ownerAgent ? { parentSpanId: ownerAgent.parentSpanId } : {}), + } + : {} + + if (node.kind === 'text') { + if (!node.text) continue + // Real wall-clock timing drives the thinking-duration UI ("Thought for Ns" + // + the 3s active-suppression); fall back to seq when ts was unavailable. + const timing = { + timestamp: node.startedAtMs ?? node.seq, + ...(node.endedAtMs !== undefined ? { endedAt: node.endedAtMs } : {}), + } + if (node.channel === 'thinking') { + entries.push({ + seq: node.seq, + block: isSub + ? { + type: 'subagent_thinking', + content: node.text, + ...(ownerAgent ? { subagent: ownerAgent.agentId } : {}), + ...spanFields, + ...timing, + } + : { type: 'thinking', content: node.text, ...timing }, + }) + } else { + entries.push({ + seq: node.seq, + block: isSub + ? { + type: 'text', + content: node.text, + ...(ownerAgent ? { subagent: ownerAgent.agentId } : {}), + ...spanFields, + ...timing, + } + : { type: 'text', content: node.text, ...timing }, + }) + } + continue + } + + if (node.kind === 'tool') { + // Per-call hidden tools are tracked for side effects but never rendered. + if (node.hidden) continue + const displayTitle = toolDisplayTitle(node) + entries.push({ + seq: node.seq, + block: { + type: 'tool_call', + toolCall: { + id: node.id, + name: node.name, + status: nodeToToolStatus(node.status), + ...(displayTitle ? { displayTitle } : {}), + ...(node.args ? { params: node.args } : {}), + ...(node.streamingArgs ? { streamingArgs: node.streamingArgs } : {}), + ...(node.result + ? { + result: { + success: node.result.success, + ...(node.result.output !== undefined ? { output: node.result.output } : {}), + ...(node.result.error ? { error: node.result.error } : {}), + }, + } + : {}), + ...(isSub && ownerAgent ? { calledBy: ownerAgent.agentId } : {}), + }, + ...spanFields, + // Wall-clock when available (uniform with text); falls back to seq. + timestamp: node.startedAtMs ?? node.seq, + }, + }) + continue + } + + // Agent node -> a `subagent` open block, plus a `subagent_end` at end seq. + entries.push({ + seq: node.seq, + block: { + type: 'subagent', + content: node.agentId, + spanId: node.spanId, + parentSpanId: node.parentSpanId, + ...(node.triggerToolCallId ? { parentToolCallId: node.triggerToolCallId } : {}), + timestamp: node.startedAtMs ?? node.seq, + }, + }) + if (node.endSeq !== undefined) { + entries.push({ + seq: node.endSeq, + block: { + type: 'subagent_end', + spanId: node.spanId, + parentSpanId: node.parentSpanId, + ...(node.triggerToolCallId ? { parentToolCallId: node.triggerToolCallId } : {}), + timestamp: node.endSeq, + }, + }) + } + } + + entries.sort((a, b) => a.seq - b.seq) + return entries.map((e) => e.block) +} + +/** Returns the assistant-channel text of the main lane, in order (snapshot `content`). */ +export function modelMainText(model: TurnModel): string { + let text = '' + for (const id of model.order) { + const node = model.nodes.get(id) + if (node?.kind === 'text' && node.spanId === MAIN_SPAN && node.channel === 'assistant') { + text += node.text + } + } + return text +} + +/** + * Rebuilds a model from a persisted/live snapshot of content blocks. Used when a + * reconnect resumes a stream whose model is not in memory (page reload mid-turn): + * the snapshot is replayed as synthetic envelopes so subsequent live events fold + * into the identical model. Operates on the live, span-carrying block shape. + */ +export function contentBlocksToModel(blocks: ContentBlock[]): TurnModel { + const model = createTurnModel() + let seq = 0 + const synth = ( + type: string, + payload: Record, + scope?: Record, + tsMs?: number + ): PersistedStreamEventEnvelope => + ({ + v: 1, + seq: ++seq, + // Carry the persisted wall-clock so the rebuilt model keeps real timing + // (thinking duration / 3s suppression) across a reconnect rebuild. + ts: tsMs !== undefined ? new Date(tsMs).toISOString() : '', + stream: { streamId: '', cursor: String(seq) }, + type, + payload, + ...(scope ? { scope } : {}), + // double-cast-allowed: synthetic replay envelope rebuilt from ContentBlocks for reduceEvent only; payloads are intentionally the minimal shape the reducer reads (no executor/mode), never provider-parsed or re-emitted on the wire + }) as unknown as PersistedStreamEventEnvelope + + const scopeFor = (block: ContentBlock): Record | undefined => + block.spanId + ? { + lane: 'subagent', + spanId: block.spanId, + ...(block.parentSpanId ? { parentSpanId: block.parentSpanId } : {}), + ...(block.parentToolCallId ? { parentToolCallId: block.parentToolCallId } : {}), + ...(block.subagent ? { agentId: block.subagent } : {}), + } + : undefined + + for (const block of blocks) { + if (block.type === 'subagent') { + reduceEvent( + model, + synth( + 'span', + { + kind: 'subagent', + event: 'start', + agent: block.content, + data: block.parentToolCallId ? { tool_call_id: block.parentToolCallId } : {}, + }, + scopeFor(block), + block.timestamp + ) + ) + if (block.endedAt !== undefined) { + reduceEvent( + model, + synth( + 'span', + { kind: 'subagent', event: 'end', agent: block.content, data: {} }, + scopeFor(block), + block.endedAt + ) + ) + } + continue + } + if (block.type === 'subagent_end') { + reduceEvent( + model, + synth('span', { kind: 'subagent', event: 'end', agent: '', data: {} }, scopeFor(block)) + ) + continue + } + if (block.type === 'tool_call' && block.toolCall) { + const tc = block.toolCall + reduceEvent( + model, + synth( + 'tool', + { + phase: 'call', + toolCallId: tc.id, + toolName: tc.name, + arguments: tc.params, + // Preserve a server-provided title that isn't derivable from args. + ...(tc.displayTitle ? { ui: { title: tc.displayTitle } } : {}), + }, + scopeFor(block), + block.timestamp + ) + ) + if (tc.status !== ToolCallStatus.executing) { + const node = toolStatusToNode(tc.status) + reduceEvent( + model, + synth( + 'tool', + { + phase: 'result', + toolCallId: tc.id, + toolName: tc.name, + success: node === 'success', + status: node, + output: tc.result?.output, + // Carry the failure message so a reloaded failed tool keeps it. + ...(tc.result?.error ? { error: tc.result.error } : {}), + }, + scopeFor(block) + ) + ) + } + continue + } + if (block.type === 'text' || block.type === 'subagent_text') { + if (block.content) { + reduceEvent( + model, + synth( + 'text', + { channel: 'assistant', text: block.content }, + scopeFor(block), + block.timestamp + ) + ) + } + continue + } + if (block.type === 'thinking' || block.type === 'subagent_thinking') { + if (block.content) { + reduceEvent( + model, + synth( + 'text', + { channel: 'thinking', text: block.content }, + scopeFor(block), + block.timestamp + ) + ) + } + } + } + + return model +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts new file mode 100644 index 00000000000..85d40741490 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts @@ -0,0 +1,486 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' +import { + type AgentNode, + applyTurnTerminal, + createTurnModel, + MAIN_SPAN, + reduceEvent, + type TextNode, + type ToolNode, + type TurnModel, +} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model' + +interface Scope { + lane: 'subagent' + spanId?: string + parentSpanId?: string + parentToolCallId?: string + agentId?: string +} + +function envelope( + seq: number, + type: string, + payload: Record, + scope?: Scope +): PersistedStreamEventEnvelope { + return { + v: 1, + seq, + ts: new Date(seq).toISOString(), + stream: { streamId: 's1', cursor: String(seq) }, + type, + payload, + ...(scope ? { scope } : {}), + } as unknown as PersistedStreamEventEnvelope +} + +function toolCall(seq: number, id: string, name: string, scope?: Scope) { + return envelope(seq, 'tool', { phase: 'call', toolCallId: id, toolName: name }, scope) +} + +function toolResult(seq: number, id: string, success: boolean, status?: string, scope?: Scope) { + return envelope( + seq, + 'tool', + { phase: 'result', toolCallId: id, toolName: 'x', success, ...(status ? { status } : {}) }, + scope + ) +} + +function spanStart( + seq: number, + spanId: string, + agent: string, + parentToolCallId?: string, + parentSpanId = MAIN_SPAN +) { + return envelope( + seq, + 'span', + { + kind: 'subagent', + event: 'start', + agent, + data: parentToolCallId ? { tool_call_id: parentToolCallId } : {}, + }, + { + lane: 'subagent', + spanId, + parentSpanId, + ...(parentToolCallId ? { parentToolCallId } : {}), + agentId: agent, + } + ) +} + +function spanEnd( + seq: number, + spanId: string, + agent: string, + opts?: { error?: string; pending?: boolean } +) { + return envelope( + seq, + 'span', + { + kind: 'subagent', + event: 'end', + agent, + data: { + ...(opts?.error ? { error: opts.error } : {}), + ...(opts?.pending ? { pending: true } : {}), + }, + }, + { lane: 'subagent', spanId, agentId: agent } + ) +} + +function textEvent(seq: number, channel: 'assistant' | 'thinking', text: string, scope?: Scope) { + return envelope(seq, 'text', { channel, text }, scope) +} + +function complete(seq: number, status: 'complete' | 'cancelled' | 'error' = 'complete') { + return envelope(seq, 'complete', { status }) +} + +function apply(events: PersistedStreamEventEnvelope[], model = createTurnModel()): TurnModel { + for (const e of events) reduceEvent(model, e) + return model +} + +function tool(model: TurnModel, id: string): ToolNode { + const node = model.nodes.get(id) + expect(node?.kind).toBe('tool') + return node as ToolNode +} + +function agent(model: TurnModel, spanId: string): AgentNode { + const node = model.nodes.get(spanId) + expect(node?.kind).toBe('agent') + return node as AgentNode +} + +describe('reduceEvent — tool lifecycle', () => { + it('runs a tool then settles it success on result', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), toolResult(2, 'tc-1', true)]) + expect(tool(m, 'tc-1').status).toBe('success') + expect(tool(m, 'tc-1').result?.success).toBe(true) + }) + + it('settles a tool error on a failed result', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), toolResult(2, 'tc-1', false)]) + expect(tool(m, 'tc-1').status).toBe('error') + }) + + it('honors an explicit terminal status over the success boolean', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), toolResult(2, 'tc-1', true, 'cancelled')]) + expect(tool(m, 'tc-1').status).toBe('cancelled') + }) + + it('accumulates streaming args across deltas', () => { + const m = apply([ + toolCall(1, 'tc-1', 'workspace_file'), + envelope(2, 'tool', { + phase: 'args_delta', + toolCallId: 'tc-1', + toolName: 'workspace_file', + argumentsDelta: '{"a":', + }), + envelope(3, 'tool', { + phase: 'args_delta', + toolCallId: 'tc-1', + toolName: 'workspace_file', + argumentsDelta: '1}', + }), + ]) + expect(tool(m, 'tc-1').streamingArgs).toBe('{"a":1}') + expect(tool(m, 'tc-1').status).toBe('running') + }) + + it('clears streamingArgs once the result settles the tool', () => { + const m = apply([ + toolCall(1, 'tc-1', 'workspace_file'), + envelope(2, 'tool', { + phase: 'args_delta', + toolCallId: 'tc-1', + toolName: 'workspace_file', + argumentsDelta: '{"operation":"create"', + }), + toolResult(3, 'tc-1', true), + ]) + expect(tool(m, 'tc-1').status).toBe('success') + expect(tool(m, 'tc-1').streamingArgs).toBeUndefined() + }) + + it('buffers a result that arrives before its call, then applies it', () => { + const m = apply([toolResult(1, 'tc-1', true), toolCall(2, 'tc-1', 'search')]) + expect(tool(m, 'tc-1').status).toBe('success') + }) + + it('preserves result.error when a result is buffered before its call', () => { + const m = apply([ + envelope(1, 'tool', { + phase: 'result', + toolCallId: 'tc-1', + toolName: 'search', + success: false, + error: 'boom', + }), + toolCall(2, 'tc-1', 'search'), + ]) + expect(tool(m, 'tc-1').status).toBe('error') + expect(tool(m, 'tc-1').result?.error).toBe('boom') + }) + + it('resolves output-based cancellation (user_cancelled) as cancelled, not error', () => { + const m = apply([ + toolCall(1, 'tc-1', 'search'), + envelope(2, 'tool', { + phase: 'result', + toolCallId: 'tc-1', + toolName: 'search', + success: false, + output: { reason: 'user_cancelled' }, + }), + ]) + expect(tool(m, 'tc-1').status).toBe('cancelled') + }) + + it('ignores preview phases (decoupled from tool status)', () => { + const m = apply([ + toolCall(1, 'tc-1', 'workspace_file'), + envelope(2, 'tool', { + previewPhase: 'file_preview_content', + toolCallId: 'tc-1', + toolName: 'workspace_file', + content: 'x', + contentMode: 'delta', + fileName: 'f', + previewVersion: 1, + }), + ]) + expect(tool(m, 'tc-1').status).toBe('running') + expect(m.order).toEqual(['tc-1']) + }) +}) + +describe('reduceEvent — subagent lifecycle', () => { + it('opens an agent run on span start and settles it on span end', () => { + const m = apply([spanStart(1, 'S1', 'file', 'tc-file'), spanEnd(2, 'S1', 'file')]) + expect(agent(m, 'S1').status).toBe('success') + expect(agent(m, 'S1').triggerToolCallId).toBe('tc-file') + expect(agent(m, 'S1').parentSpanId).toBe(MAIN_SPAN) + }) + + it('settles an agent error when span end carries an error', () => { + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-file'), + spanEnd(2, 'S1', 'file', { error: 'boom' }), + ]) + expect(agent(m, 'S1').status).toBe('error') + }) + + it('keeps an agent running on a pending-pause span end', () => { + const m = apply([ + spanStart(1, 'S1', 'deploy', 'tc-deploy'), + spanEnd(2, 'S1', 'deploy', { pending: true }), + ]) + expect(agent(m, 'S1').status).toBe('running') + }) + + it('nests a child run under its parent by parentSpanId', () => { + const m = apply([ + spanStart(1, 'S1', 'workflow', 'tc-wf'), + spanStart(2, 'S2', 'deploy', 'tc-deploy', 'S1'), + spanEnd(3, 'S2', 'deploy'), + spanEnd(4, 'S1', 'workflow'), + ]) + expect(agent(m, 'S2').parentSpanId).toBe('S1') + expect(agent(m, 'S1').parentSpanId).toBe(MAIN_SPAN) + expect(agent(m, 'S1').status).toBe('success') + expect(agent(m, 'S2').status).toBe('success') + }) + + it('keeps two parallel same-name runs independent (no agentId collision)', () => { + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-a'), + spanStart(2, 'S2', 'file', 'tc-b'), + toolCall(3, 'wf-a', 'workspace_file', { lane: 'subagent', spanId: 'S1' }), + toolCall(4, 'wf-b', 'workspace_file', { lane: 'subagent', spanId: 'S2' }), + toolResult(5, 'wf-a', true), + spanEnd(6, 'S1', 'file'), + toolResult(7, 'wf-b', true), + spanEnd(8, 'S2', 'file'), + ]) + expect(agent(m, 'S1').triggerToolCallId).toBe('tc-a') + expect(agent(m, 'S2').triggerToolCallId).toBe('tc-b') + expect(tool(m, 'wf-a').spanId).toBe('S1') + expect(tool(m, 'wf-b').spanId).toBe('S2') + expect(agent(m, 'S1').status).toBe('success') + expect(agent(m, 'S2').status).toBe('success') + }) +}) + +describe('reduceEvent — text segmentation', () => { + it('records wall-clock start/end for a thinking segment from wire ts', () => { + // envelope() stamps ts = new Date(seq).toISOString(), so tsMs === seq here. + const m = apply([ + textEvent(1, 'thinking', 'pondering'), + textEvent(2, 'assistant', 'the answer'), + ]) + const thinking = [...m.nodes.values()].find( + (n) => n.kind === 'text' && n.channel === 'thinking' + ) as TextNode + expect(thinking.startedAtMs).toBe(1) + // The answer starting closes the thinking segment, bounding its duration. + expect(thinking.endedAtMs).toBe(2) + }) + + it('merges contiguous deltas and splits across a tool boundary', () => { + const m = apply([ + textEvent(1, 'assistant', 'Hello '), + textEvent(2, 'assistant', 'world'), + toolCall(3, 'tc-1', 'search'), + toolResult(4, 'tc-1', true), + textEvent(5, 'assistant', 'after'), + ]) + const texts = m.order.map((id) => m.nodes.get(id)).filter((n) => n?.kind === 'text') + expect(texts.map((t) => (t as { text: string }).text)).toEqual(['Hello world', 'after']) + }) +}) + +describe('reduceEvent — idempotency', () => { + it('is a no-op for an already-applied seq (reconnect replay over a populated model)', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), toolResult(2, 'tc-1', true)]) + const before = JSON.stringify([...m.nodes]) + reduceEvent(m, toolCall(1, 'tc-1', 'search')) + reduceEvent(m, toolResult(2, 'tc-1', true)) + expect(JSON.stringify([...m.nodes])).toBe(before) + expect(m.order).toEqual(['tc-1']) + }) + + it('rebuilds the identical model when replayed into a fresh model', () => { + const events = [ + spanStart(1, 'S1', 'file', 'tc-file'), + toolCall(2, 'wf', 'workspace_file', { lane: 'subagent', spanId: 'S1' }), + toolResult(3, 'wf', true), + spanEnd(4, 'S1', 'file'), + complete(5), + ] + const live = apply(events) + const replayed = apply(events, createTurnModel()) + expect([...replayed.nodes]).toEqual([...live.nodes]) + expect(replayed.order).toEqual(live.order) + expect(replayed.status).toBe(live.status) + }) +}) + +describe('reduceEvent — edit_content row merge', () => { + it('folds an edit_content write into its span workspace_file row', () => { + const sub: Scope = { lane: 'subagent', spanId: 'S1' } + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-file'), + toolCall(2, 'wf-1', 'workspace_file', sub), + toolResult(3, 'wf-1', true, undefined, sub), + toolCall(4, 'ec-1', 'edit_content', sub), + ]) + // No separate edit_content node; the workspace_file row reopened for the edit. + expect(m.nodes.has('ec-1')).toBe(false) + expect(tool(m, 'wf-1').status).toBe('running') + expect(m.toolAlias.get('ec-1')).toBe('wf-1') + }) + + it('settles the merged row on the edit_content result', () => { + const sub: Scope = { lane: 'subagent', spanId: 'S1' } + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-file'), + toolCall(2, 'wf-1', 'workspace_file', sub), + toolCall(3, 'ec-1', 'edit_content', sub), + toolResult(4, 'ec-1', true, undefined, sub), + ]) + expect(tool(m, 'wf-1').status).toBe('success') + expect(m.nodes.has('ec-1')).toBe(false) + }) + + it('folds an edit_content result that raced ahead of its call into the merged row', () => { + const sub: Scope = { lane: 'subagent', spanId: 'S1' } + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-file'), + toolCall(2, 'wf-1', 'workspace_file', sub), + // Result for edit_content arrives BEFORE its call (buffered under ec-1)... + toolResult(3, 'ec-1', true, undefined, sub), + // ...then the call lands and aliases ec-1 -> wf-1, draining the buffer. + toolCall(4, 'ec-1', 'edit_content', sub), + ]) + expect(tool(m, 'wf-1').status).toBe('success') + expect(tool(m, 'wf-1').result?.success).toBe(true) + expect(m.bufferedResults.has('ec-1')).toBe(false) + }) + + it('finalizes a stale running section row when the next section opens', () => { + const sub: Scope = { lane: 'subagent', spanId: 'S1' } + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-file'), + // Section 1: the workspace_file row is reopened by its edit_content, but the + // edit's closing result is reordered/dropped — wf-1 is left running. + toolCall(2, 'wf-1', 'workspace_file', sub), + toolResult(3, 'wf-1', true, undefined, sub), + toolCall(4, 'ec-1', 'edit_content', sub), + // Section 2 opens before section 1's edit result lands. + toolCall(5, 'wf-2', 'workspace_file', sub), + ]) + // The previous section settles instead of spinning until the turn terminal... + expect(tool(m, 'wf-1').status).toBe('success') + // ...and the new section's row is the live write. + expect(tool(m, 'wf-2').status).toBe('running') + }) +}) + +describe('reduceEvent — error tag + compaction coverage', () => { + it('appends an inline mothership-error tag to the scoped lane text', () => { + const m = apply([ + textEvent(1, 'assistant', 'Working'), + envelope(2, 'error', { message: 'boom', code: 'E1', provider: 'openai' }), + ]) + const text = [...m.nodes.values()].find((n) => n.kind === 'text') as { text: string } + expect(text.text).toContain('') + expect(text.text).toContain('boom') + expect(text.text).toContain('E1') + }) + + it('does not duplicate an identical error tag', () => { + const m = apply([ + textEvent(1, 'assistant', 'Working'), + envelope(2, 'error', { message: 'boom' }), + envelope(3, 'error', { message: 'boom' }), + ]) + const text = [...m.nodes.values()].find((n) => n.kind === 'text') as { text: string } + const occurrences = text.text.split('').length - 1 + expect(occurrences).toBe(1) + }) + + it('opens and closes a compaction node with titles', () => { + const m = apply([ + envelope(1, 'run', { kind: 'compaction_start' }), + envelope(2, 'run', { kind: 'compaction_done' }), + ]) + const compaction = [...m.nodes.values()].find( + (n) => n.kind === 'tool' && n.name === 'context_compaction' + ) as ToolNode + expect(compaction.status).toBe('success') + expect(compaction.uiTitle).toBe('Compacted context') + }) +}) + +describe('turn-terminal propagation', () => { + it('settles stragglers as success on a clean complete (never interrupted)', () => { + const m = apply([ + toolCall(1, 'tc-1', 'search'), + spanStart(2, 'S1', 'file', 'tc-file'), + complete(3, 'complete'), + ]) + expect(m.status).toBe('complete') + expect(tool(m, 'tc-1').status).toBe('success') + expect(agent(m, 'S1').status).toBe('success') + for (const node of m.nodes.values()) { + expect(node.kind === 'text' || node.status).not.toBe('interrupted') + } + }) + + it('closes a straggler subagent lane (sets endSeq) so a model-driven terminal resolves the group', () => { + // A file subagent opened but no span end arrived (mid-stream error/disconnect). + const m = apply([ + spanStart(1, 'S1', 'file', 'tc-file'), + toolCall(2, 'wf-1', 'workspace_file', { lane: 'subagent', spanId: 'S1' }), + ]) + expect(agent(m, 'S1').endSeq).toBeUndefined() + applyTurnTerminal(m, 'error') + expect(agent(m, 'S1').status).toBe('error') + // endSeq must be stamped so the serializer emits subagent_end and the lane's + // delegating spinner resolves instead of spinning forever. + expect(agent(m, 'S1').endSeq).toBeDefined() + }) + + it('settles open nodes cancelled on a stop', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), complete(2, 'cancelled')]) + expect(m.status).toBe('cancelled') + expect(tool(m, 'tc-1').status).toBe('cancelled') + }) + + it('settles open nodes error on an errored turn', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), complete(2, 'error')]) + expect(m.status).toBe('error') + expect(tool(m, 'tc-1').status).toBe('error') + }) + + it('never reopens an already-terminal node', () => { + const m = apply([toolCall(1, 'tc-1', 'search'), toolResult(2, 'tc-1', false)]) + applyTurnTerminal(m, 'complete') + expect(tool(m, 'tc-1').status).toBe('error') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts new file mode 100644 index 00000000000..3a2fd46e400 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts @@ -0,0 +1,656 @@ +import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome' +import { + MothershipStreamV1CompletionStatus, + MothershipStreamV1EventType, + MothershipStreamV1RunKind, + MothershipStreamV1SpanLifecycleEvent, + MothershipStreamV1SpanPayloadKind, + MothershipStreamV1ToolPhase, +} from '@/lib/copilot/generated/mothership-stream-v1' +import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' + +/** + * The single deterministic model of one assistant turn, derived purely from the + * Go wire stream. Every tool and subagent is a {@link LifecycleNode} with one + * explicit terminal source, so rendering reads node status instead of inferring + * it from transport, preview sessions, or a turn-complete sweep. Parallel + * subagents are independent span lanes; nested subagents nest by `parentSpanId`. + */ + +/** The root span lane id Go stamps on main-agent (non-subagent) events. */ +export const MAIN_SPAN = 'main' + +/** + * Terminal-bearing status for a single node. `running` is the only + * non-terminal value; everything else is read from an explicit wire terminal + * (tool `result`, span `end`) or propagated from the turn terminal. + */ +export type NodeStatus = 'running' | 'success' | 'error' | 'cancelled' | 'skipped' | 'rejected' + +/** Turn-level status. Terminal values come from the wire `complete`/`error`. */ +export type TurnStatus = 'streaming' | 'complete' | 'error' | 'cancelled' + +export type TextChannel = 'assistant' | 'thinking' + +interface NodeBase { + /** Stable node id. Tools use `toolCallId`; agents use `spanId`; text/synthetic use a derived id. */ + id: string + /** The span lane this node belongs to (`MAIN_SPAN` for the main agent). */ + spanId: string + /** Arrival order key (wire `seq`), monotonic within a turn. */ + seq: number + /** + * Wall-clock (wire `ts`) the node opened. Serialized as the block `timestamp` + * so it always means epoch-ms (never the wire seq), driving duration UI and + * surviving the reconnect round-trip. Absent only when `ts` was unavailable. + */ + startedAtMs?: number +} + +export interface ToolNode extends NodeBase { + kind: 'tool' + name: string + status: NodeStatus + args?: Record + streamingArgs?: string + uiTitle?: string + /** Per-call `ui.hidden` flag — the node is tracked for side effects but not rendered. */ + hidden?: boolean + result?: { success: boolean; output?: unknown; error?: string } +} + +export interface AgentNode extends NodeBase { + kind: 'agent' + /** Span lane of the run that invoked this one (`MAIN_SPAN` for a direct child). */ + parentSpanId: string + /** Display id (e.g. `file`, `workflow`) — never a routing key (collides across siblings). */ + agentId: string + /** The outer delegation tool_use that triggered this run; links the trigger tool node. */ + triggerToolCallId?: string + status: NodeStatus + /** Wire seq at which the run terminated (span end), for ordering the close marker. */ + endSeq?: number +} + +export interface TextNode extends NodeBase { + kind: 'text' + channel: TextChannel + text: string + /** Wall-clock (wire `ts`) the segment was superseded by the next lane content. */ + endedAtMs?: number +} + +export type LifecycleNode = ToolNode | AgentNode | TextNode + +export interface TurnModel { + status: TurnStatus + /** All nodes by id. */ + nodes: Map + /** Node ids in arrival order — the projection orders within a lane by this. */ + order: string[] + /** spanId -> agent node id (always equal to spanId). */ + agentBySpanId: Map + /** `${spanId}::${channel}` -> currently-open text node id (cleared on a lane break). */ + openTextByKey: Map + /** + * Results that arrived before their tool `call` (out-of-order), keyed by + * toolCallId. Raw `status`/`output` are kept so the outcome (incl. output-based + * cancellation) resolves identically to the in-order path when drained. + */ + bufferedResults: Map< + string, + { success: boolean; output?: unknown; status?: unknown; error?: string } + > + /** + * Maps a tool call id to another tool node it folds into. Used for the + * `edit_content` -> `workspace_file` row merge so the write streams into the + * single "writing" row rather than a second row. + */ + toolAlias: Map + /** Highest applied wire seq; events at or below are no-ops (cursor-idempotent replay). */ + lastSeq: number +} + +export function createTurnModel(): TurnModel { + return { + status: 'streaming', + nodes: new Map(), + order: [], + agentBySpanId: new Map(), + openTextByKey: new Map(), + bufferedResults: new Map(), + toolAlias: new Map(), + lastSeq: 0, + } +} + +const WORKSPACE_FILE_TOOL = 'workspace_file' +const EDIT_CONTENT_TOOL = 'edit_content' + +/** Resolves a tool call id through the alias map (e.g. edit_content -> its workspace_file row). */ +export function resolveToolId(model: TurnModel, id: string): string { + return model.toolAlias.get(id) ?? id +} + +/** + * Finds the most recent `workspace_file` tool node in a span so an `edit_content` + * write folds into it (the single "writing" row). Co-location in the file + * subagent's span is the link — no coupling to preview phases. The caller + * reopens whatever this returns, including an already-settled row (an edit after + * a completed write is the same file operation continuing), which is the + * intended single-row behavior, not the old preview-gated parent reuse. + */ +function findWorkspaceFileNodeInSpan(model: TurnModel, spanId: string): ToolNode | undefined { + for (let i = model.order.length - 1; i >= 0; i--) { + const node = model.nodes.get(model.order[i]) + if (node?.kind === 'tool' && node.spanId === spanId && node.name === WORKSPACE_FILE_TOOL) { + return node + } + } + return undefined +} + +/** + * The file agent writes a file as strictly sequential `workspace_file` + + * `edit_content` section pairs, waiting for each to finish before the next. So + * when a new section's `workspace_file` opens, any earlier `workspace_file` row + * still `running` in the same span is a completed section whose closing + * `edit_content` result was reordered or dropped — finalize it as success so its + * "writing" spinner resolves when the next section starts, instead of lingering + * until the turn-terminal sweep. A no-op on the happy path (prior rows already + * settled on their own result). + */ +function finalizeStaleWorkspaceFiles(model: TurnModel, spanId: string): void { + for (const id of model.order) { + const node = model.nodes.get(id) + if ( + node?.kind === 'tool' && + node.spanId === spanId && + node.name === WORKSPACE_FILE_TOOL && + node.status === 'running' + ) { + node.status = 'success' + node.streamingArgs = undefined + } + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function asString(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined +} + +/** + * Reads a wire event payload as a generic record. The payload is a wide + * discriminated union; the reducer accesses fields uniformly, so this narrows + * through the `unknown`-typed {@link isRecord} guard rather than a double cast. + */ +function payloadRecord(payload: unknown): Record { + return isRecord(payload) ? payload : {} +} + +/** Parses a wire `ts` to epoch ms, or undefined when absent/unparseable. */ +function tsToMs(ts: unknown): number | undefined { + if (typeof ts !== 'string' || ts === '') return undefined + const ms = Date.parse(ts) + return Number.isFinite(ms) ? ms : undefined +} + +const TERMINAL_NODE_STATUSES = new Set([ + 'success', + 'error', + 'cancelled', + 'skipped', + 'rejected', +]) + +export function isNodeTerminal(status: NodeStatus): boolean { + return TERMINAL_NODE_STATUSES.has(status) +} + +/** Maps the wire turn-completion status to the status propagated to open nodes. */ +function turnTerminalNodeStatus(turn: Exclude): NodeStatus { + if (turn === 'cancelled') return 'cancelled' + if (turn === 'error') return 'error' + return 'success' +} + +/** + * Builds the inline `` tag rendered for a stream error. Kept + * byte-identical to the prior `buildInlineErrorTag` so the error special-tag + * parser renders it the same way. + */ +function buildMothershipErrorTag(payload: Record): string { + const message = + asString(payload.displayMessage) ?? + asString(payload.message) ?? + asString(payload.error) ?? + 'An unexpected error occurred' + const provider = asString(payload.provider) + const code = asString(payload.code) + return `${JSON.stringify({ + message, + ...(code ? { code } : {}), + ...(provider ? { provider } : {}), + })}` +} + +/** Closes a span's open text segment for `channel`, stamping its end time. */ +function closeOpenText( + model: TurnModel, + spanId: string, + channel: TextChannel, + atMs?: number +): void { + const key = `${spanId}::${channel}` + const nodeId = model.openTextByKey.get(key) + if (!nodeId) return + if (atMs !== undefined) { + const node = model.nodes.get(nodeId) + if (node?.kind === 'text' && node.endedAtMs === undefined) node.endedAtMs = atMs + } + model.openTextByKey.delete(key) +} + +/** Drops any open text segments in a lane so the next text starts a fresh node. */ +function breakLane(model: TurnModel, spanId: string, atMs?: number): void { + closeOpenText(model, spanId, 'assistant', atMs) + closeOpenText(model, spanId, 'thinking', atMs) +} + +function appendText( + model: TurnModel, + spanId: string, + channel: TextChannel, + text: string, + seq: number, + atMs?: number +): void { + if (!text) return + const key = `${spanId}::${channel}` + const openId = model.openTextByKey.get(key) + const open = openId ? model.nodes.get(openId) : undefined + if (open && open.kind === 'text') { + open.text += text + return + } + // A new segment supersedes the other channel's open text (e.g. the answer + // starts after thinking), which bounds the thinking segment's duration. + closeOpenText(model, spanId, channel === 'thinking' ? 'assistant' : 'thinking', atMs) + const node: TextNode = { + kind: 'text', + id: `text:${seq}`, + spanId, + channel, + text, + seq, + ...(atMs !== undefined ? { startedAtMs: atMs } : {}), + } + model.nodes.set(node.id, node) + model.order.push(node.id) + model.openTextByKey.set(key, node.id) +} + +/** + * Applies a result that raced ahead of its tool `call` (buffered under `fromId`) + * onto `node`, then clears the buffer. Used by the normal call path and by the + * edit_content -> workspace_file merge, where the buffer is keyed by the + * edit_content id but folds into the workspace_file row. + */ +function drainBufferedResult(model: TurnModel, fromId: string, node: ToolNode): void { + const buffered = model.bufferedResults.get(fromId) + if (!buffered) return + model.bufferedResults.delete(fromId) + node.status = resolveStreamToolOutcome({ + status: asString(buffered.status), + success: buffered.success, + output: buffered.output, + }) + node.result = { + success: buffered.success, + output: buffered.output, + ...(buffered.error ? { error: buffered.error } : {}), + } + node.streamingArgs = undefined +} + +function upsertToolNode( + model: TurnModel, + id: string, + spanId: string, + name: string, + seq: number, + atMs?: number +): ToolNode { + const existing = model.nodes.get(id) + if (existing && existing.kind === 'tool') { + if (name && !existing.name) existing.name = name + return existing + } + const node: ToolNode = { + kind: 'tool', + id, + spanId, + name, + status: 'running', + seq, + ...(atMs !== undefined ? { startedAtMs: atMs } : {}), + } + model.nodes.set(id, node) + model.order.push(id) + // A tool starting (or any structural event) closes the current text run. + breakLane(model, spanId, atMs) + drainBufferedResult(model, id, node) + return node +} + +function applyToolResult( + model: TurnModel, + id: string, + success: boolean, + status: unknown, + output: unknown, + error: string | undefined +): void { + const existing = model.nodes.get(id) + if (existing && existing.kind === 'tool') { + existing.status = resolveStreamToolOutcome({ status: asString(status), success, output }) + existing.result = { success, output, ...(error ? { error } : {}) } + // The args have fully resolved; drop the partial stream so the title and + // any re-serialization read the final args, not truncated streaming JSON. + existing.streamingArgs = undefined + return + } + // Result before call: buffer raw fields until the call materializes the node; + // the outcome (incl. output-based cancellation) is resolved on drain. + model.bufferedResults.set(id, { success, output, status, ...(error ? { error } : {}) }) +} + +/** + * Materializes a subagent lane on first reference. Subagent-scoped content + * (text/thinking/tool) can be reduced before its `subagent_start` under heavy + * parallel bursts (many subagents streaming into one ordered channel); without + * the owning `AgentNode` the serializer can't attribute the content, so it leaks + * into the main lane and the subagent's thinking is dropped until the start + * lands. The wire scope already carries the lane identity (Go tags every + * forwarded subagent event with its agent id/span), so the lane is rebuilt + * deterministically from the content event itself — the symmetric counterpart to + * buffering a result before its call. The later `subagent_start` finds this node + * and no-ops. + */ +function ensureSubagentLane( + model: TurnModel, + spanId: string, + scope: { agentId?: string; parentSpanId?: string; parentToolCallId?: string } | undefined, + seq: number, + atMs?: number +): void { + if (spanId === MAIN_SPAN || model.agentBySpanId.has(spanId)) return + const node: AgentNode = { + kind: 'agent', + id: spanId, + spanId, + parentSpanId: scope?.parentSpanId ?? MAIN_SPAN, + agentId: scope?.agentId ?? '', + status: 'running', + seq, + ...(atMs !== undefined ? { startedAtMs: atMs } : {}), + ...(scope?.parentToolCallId ? { triggerToolCallId: scope.parentToolCallId } : {}), + } + model.nodes.set(node.id, node) + model.order.push(node.id) + model.agentBySpanId.set(spanId, node.id) +} + +/** + * Folds one wire envelope into the model. Pure accumulator: it mutates and + * returns the same `model` (the streaming hot path keeps one model per turn). + * `seq` is the monotonic wire cursor — the contract guarantees it is always a + * finite number — so it is the sole ordering and idempotency key: an event at + * or below the applied high-water mark is a replay and no-ops (reconnect replay + * over a populated model is a no-op; replay into a fresh model rebuilds the + * identical tree). + */ +export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnvelope): TurnModel { + const seq = envelope.seq + if (seq <= model.lastSeq) return model + model.lastSeq = seq + const tsMs = tsToMs(envelope.ts) + const scope = envelope.scope + const spanId = scope?.spanId ?? MAIN_SPAN + + switch (envelope.type) { + case MothershipStreamV1EventType.text: { + const payload = envelope.payload + ensureSubagentLane(model, spanId, scope, seq, tsMs) + appendText(model, spanId, payload.channel as TextChannel, payload.text, seq, tsMs) + break + } + case MothershipStreamV1EventType.tool: { + const payload = payloadRecord(envelope.payload) + // Preview phases are a separate panel concern (decoupled from tool status). + if ('previewPhase' in payload) break + const rawToolCallId = asString(payload.toolCallId) + if (!rawToolCallId) break + const toolName = asString(payload.toolName) ?? '' + ensureSubagentLane(model, spanId, scope, seq, tsMs) + const phase = payload.phase + if (phase === MothershipStreamV1ToolPhase.call) { + // edit_content folds into its span's workspace_file row (the write + // continues in the single "writing" row), reopening it for the edit. + if (toolName === EDIT_CONTENT_TOOL) { + // A re-emitted edit_content call (same tool call id — duplicate/replay) + // must keep its ORIGINAL target row. Re-running the span lookup can + // return a newer workspace_file, and folding into that would leave the + // first (already reopened) row running with no result ever closing it — + // a spinner stuck until the turn-terminal sweep. So once aliased, reuse. + const aliasedId = model.toolAlias.get(rawToolCallId) + const aliasedParent = aliasedId ? model.nodes.get(aliasedId) : undefined + const parent = + aliasedParent?.kind === 'tool' + ? aliasedParent + : findWorkspaceFileNodeInSpan(model, spanId) + if (parent) { + model.toolAlias.set(rawToolCallId, parent.id) + parent.status = 'running' + parent.result = undefined + // A result that raced ahead of this call was buffered under the + // edit_content id; fold it into the reopened workspace_file row. + drainBufferedResult(model, rawToolCallId, parent) + break + } + } + // A new file section opening settles any earlier still-running section row + // in this span (the file agent writes sections sequentially). + if (toolName === WORKSPACE_FILE_TOOL && !model.nodes.has(rawToolCallId)) { + finalizeStaleWorkspaceFiles(model, spanId) + } + const node = upsertToolNode( + model, + resolveToolId(model, rawToolCallId), + spanId, + toolName, + seq, + tsMs + ) + if (isRecord(payload.arguments)) node.args = payload.arguments + const ui = isRecord(payload.ui) ? payload.ui : undefined + const uiTitle = ui ? (asString(ui.title) ?? asString(ui.phaseLabel)) : undefined + if (uiTitle) node.uiTitle = uiTitle + if (ui?.hidden === true) node.hidden = true + } else if (phase === MothershipStreamV1ToolPhase.args_delta) { + const node = upsertToolNode( + model, + resolveToolId(model, rawToolCallId), + spanId, + toolName, + seq, + tsMs + ) + const delta = asString(payload.argumentsDelta) + if (delta) node.streamingArgs = (node.streamingArgs ?? '') + delta + } else if (phase === MothershipStreamV1ToolPhase.result) { + applyToolResult( + model, + resolveToolId(model, rawToolCallId), + payload.success === true, + payload.status, + payload.output, + asString(payload.error) + ) + } + break + } + case MothershipStreamV1EventType.span: { + const payload = envelope.payload + if (payload.kind !== MothershipStreamV1SpanPayloadKind.subagent) break + const data = isRecord(payload.data) ? payload.data : undefined + const triggerToolCallId = + scope?.parentToolCallId ?? asString(data?.tool_call_id) ?? asString(data?.toolCallId) + const agentId = asString(payload.agent) ?? scope?.agentId ?? '' + const resolvedSpanId = + scope?.spanId ?? (triggerToolCallId ? `span:${triggerToolCallId}` : `span:${seq}`) + const parentSpanId = scope?.parentSpanId ?? MAIN_SPAN + + if (payload.event === MothershipStreamV1SpanLifecycleEvent.start) { + breakLane(model, parentSpanId, tsMs) + const existingId = model.agentBySpanId.get(resolvedSpanId) + if (existingId && model.nodes.has(existingId)) break + const node: AgentNode = { + kind: 'agent', + id: resolvedSpanId, + spanId: resolvedSpanId, + parentSpanId, + agentId, + status: 'running', + seq: seq, + ...(tsMs !== undefined ? { startedAtMs: tsMs } : {}), + ...(triggerToolCallId ? { triggerToolCallId } : {}), + } + model.nodes.set(node.id, node) + model.order.push(node.id) + model.agentBySpanId.set(resolvedSpanId, node.id) + } else if (payload.event === MothershipStreamV1SpanLifecycleEvent.end) { + // A pending pause is not a terminal — the run resumes later. + if (data?.pending === true) break + breakLane(model, resolvedSpanId, tsMs) + const node = model.nodes.get(resolvedSpanId) + if (node && node.kind === 'agent' && !isNodeTerminal(node.status)) { + node.status = data && asString(data.error) ? 'error' : 'success' + node.endSeq = seq + } + } + break + } + case MothershipStreamV1EventType.run: { + const payload = payloadRecord(envelope.payload) + const kind = payload.kind + if (kind === MothershipStreamV1RunKind.compaction_start) { + const node = upsertToolNode( + model, + `compaction:${seq}`, + spanId, + 'context_compaction', + seq, + tsMs + ) + node.uiTitle = 'Compacting context...' + } else if (kind === MothershipStreamV1RunKind.compaction_done) { + let finalized = false + for (let i = model.order.length - 1; i >= 0; i--) { + const node = model.nodes.get(model.order[i]) + if ( + node?.kind === 'tool' && + node.name === 'context_compaction' && + node.status === 'running' + ) { + node.status = 'success' + node.uiTitle = 'Compacted context' + finalized = true + break + } + } + if (!finalized) { + const node = upsertToolNode( + model, + `compaction:${seq}`, + spanId, + 'context_compaction', + seq, + tsMs + ) + node.status = 'success' + node.uiTitle = 'Compacted context' + } + } + break + } + case MothershipStreamV1EventType.error: { + // The error tag is content (rendered inline by the error special-tag); turn + // termination on error is applied by the stream loop's terminal handling, + // not here, so a non-fatal mid-stream error event never settles the turn. + const tag = buildMothershipErrorTag(payloadRecord(envelope.payload)) + const key = `${spanId}::assistant` + const openId = model.openTextByKey.get(key) + const open = openId ? model.nodes.get(openId) : undefined + if (open && open.kind === 'text') { + if (!open.text.includes(tag)) { + const prefix = open.text.length > 0 && !open.text.endsWith('\n') ? '\n' : '' + open.text += prefix + tag + } + } else { + appendText(model, spanId, 'assistant', tag, seq, tsMs) + } + break + } + case MothershipStreamV1EventType.complete: { + const payload = payloadRecord(envelope.payload) + // An async pause is not a turn terminal — the paused tools/subagents + // legitimately stay open until a later resume leg completes them. + const response = isRecord(payload.response) ? payload.response : undefined + if (response && 'async_pause' in response) break + const status = payload.status + if (status === MothershipStreamV1CompletionStatus.cancelled) { + applyTurnTerminal(model, 'cancelled') + } else if (status === MothershipStreamV1CompletionStatus.error) { + applyTurnTerminal(model, 'error') + } else { + applyTurnTerminal(model, 'complete') + } + break + } + default: + break + } + return model +} + +/** + * Sets the turn terminal and propagates it to every still-running node. This is + * the deterministic replacement for the old `interrupted` sweep: a clean + * `complete` settles stragglers as `success` (the turn succeeded), a stop as + * `cancelled`, an error as `error`. With explicit tool/span terminals there are + * normally no stragglers, so this is the abort/disconnect safety net, not a + * routine path. + */ +export function applyTurnTerminal(model: TurnModel, turn: Exclude): void { + model.status = turn + const nodeStatus = turnTerminalNodeStatus(turn) + for (const id of model.order) { + const node = model.nodes.get(id) + if (!node || node.kind === 'text') continue + if (node.status === 'running') { + node.status = nodeStatus + // Close a straggler subagent lane (no explicit span end) so the serializer + // emits its `subagent_end` and the group resolves — otherwise the + // delegating spinner spins forever after a model-driven terminal + // (error/disconnect), the bug the snapshot path closes via `endedAt`. + if (node.kind === 'agent' && node.endSeq === undefined) { + node.endSeq = model.lastSeq + } + } + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index dc5ae5d86d2..4a56b4cd943 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -58,6 +58,7 @@ import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { useFilePreviewController } from '@/app/workspace/[workspaceId]/home/hooks/preview' import { + applyTurnTerminal, createStreamLoopContext, dispatchStreamEvent, finalizeResidualToolCalls, @@ -2010,9 +2011,7 @@ export function useChat( if (parsed.stream?.streamId) { streamIdRef.current = parsed.stream.streamId } - const eventCursor = - parsed.stream?.cursor ?? - (typeof parsed.seq === 'number' ? String(parsed.seq) : undefined) + const eventCursor = parsed.stream?.cursor ?? String(parsed.seq) if (isAlreadyProcessedStreamCursor(eventCursor, lastCursorRef.current)) { continue } @@ -2025,7 +2024,7 @@ export function useChat( } } finally { if (state.sawStreamError && !state.sawCompleteEvent) { - finalizeResidualToolCalls(state.blocks, 'error') + applyTurnTerminal(state.model, 'error') ops.flush() } if (state.scheduledTextFlushFrame !== null) { @@ -2914,6 +2913,36 @@ export function useChat( const finalize = useCallback( (options?: { error?: boolean; targetChatId?: string }) => { const isError = !!options?.error + if (isError) { + const blocks = streamingBlocksRef.current + if (blocks.some((block) => block.toolCall?.status === 'executing')) { + finalizeResidualToolCalls(blocks, 'error') + const assistantId = + activeTurnRef.current?.assistantMessageId ?? + (streamIdRef.current ? getLiveAssistantMessageId(streamIdRef.current) : undefined) + const activeChatId = options?.targetChatId ?? chatIdRef.current + if (assistantId && activeChatId) { + const snapshot = buildAssistantSnapshotMessage({ + id: assistantId, + content: streamingContentRef.current, + contentBlocks: blocks, + ...(streamRequestIdRef.current ? { requestId: streamRequestIdRef.current } : {}), + }) + upsertChatHistory(activeChatId, (current) => ({ + ...current, + messages: current.messages.map((message) => + message.id === assistantId ? snapshot : message + ), + })) + } else if (assistantId) { + setPendingMessages((prev) => + prev.map((message) => + message.id === assistantId ? { ...message, contentBlocks: [...blocks] } : message + ) + ) + } + } + } const queue = useMothershipQueueStore.getState().queues[chatKeyRef.current] const hasQueuedFollowUp = !isError && (queue?.length ?? 0) > 0 reconcileTerminalPreviewSessions() @@ -2934,6 +2963,7 @@ export function useChat( notifyTurnEnded, reconcileTerminalPreviewSessions, setTransportIdle, + upsertChatHistory, ] ) finalizeRef.current = finalize diff --git a/apps/sim/blocks/blocks/sportmonks.ts b/apps/sim/blocks/blocks/sportmonks.ts new file mode 100644 index 00000000000..4939bd51778 --- /dev/null +++ b/apps/sim/blocks/blocks/sportmonks.ts @@ -0,0 +1,818 @@ +import { SportmonksIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' + +const DATE_WAND_CONFIG = { + enabled: true, + prompt: `Generate a calendar date in YYYY-MM-DD format based on the user's description. +Examples: +- "today" -> today's date in YYYY-MM-DD +- "this weekend" -> the date of the upcoming Sunday in YYYY-MM-DD + +Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the date (e.g., "today", "this weekend")...', + generationType: 'timestamp' as const, +} + +const FOOTBALL_OPS = [ + 'football_get_livescores', + 'football_get_inplay_livescores', + 'football_get_fixtures_by_date', + 'football_get_fixtures_by_date_range', + 'football_get_fixture', + 'football_get_head_to_head', + 'football_get_leagues', + 'football_get_league', + 'football_search_teams', + 'football_get_team', + 'football_get_team_squad', + 'football_search_players', + 'football_get_player', + 'football_get_standings_by_season', + 'football_get_topscorers_by_season', +] + +const MOTORSPORT_OPS = [ + 'motorsport_get_livescores', + 'motorsport_get_fixtures_by_date', + 'motorsport_get_fixture', + 'motorsport_get_drivers', + 'motorsport_get_driver', + 'motorsport_search_drivers', + 'motorsport_get_teams', + 'motorsport_get_team', + 'motorsport_get_driver_standings_by_season', + 'motorsport_get_team_standings_by_season', + 'motorsport_get_laps_by_fixture', + 'motorsport_get_pitstops_by_fixture', +] + +const CORE_GEO_OPS = [ + 'core_get_continents', + 'core_get_continent', + 'core_get_countries', + 'core_get_country', + 'core_search_countries', + 'core_get_regions', + 'core_get_region', + 'core_get_cities', + 'core_get_city', + 'core_search_cities', +] + +const ODDS_FIXTURE_OPS = ['odds_get_pre_match_odds_by_fixture', 'odds_get_inplay_odds_by_fixture'] + +const INCLUDE_OPS = [...FOOTBALL_OPS, ...MOTORSPORT_OPS, ...ODDS_FIXTURE_OPS, ...CORE_GEO_OPS] + +const FILTER_OPS = [ + ...FOOTBALL_OPS, + ...MOTORSPORT_OPS, + ...ODDS_FIXTURE_OPS, + 'odds_get_bookmakers', + 'odds_get_markets', + 'core_get_continents', + 'core_get_countries', + 'core_get_regions', + 'core_get_cities', +] + +const PAGINATED_OPS = [ + 'football_get_fixtures_by_date', + 'football_get_fixtures_by_date_range', + 'football_get_head_to_head', + 'football_get_leagues', + 'football_search_teams', + 'football_search_players', + 'football_get_topscorers_by_season', + 'motorsport_get_livescores', + 'motorsport_get_fixtures_by_date', + 'motorsport_get_drivers', + 'motorsport_search_drivers', + 'motorsport_get_teams', + 'motorsport_get_driver_standings_by_season', + 'motorsport_get_team_standings_by_season', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_inplay_odds_by_fixture', + 'odds_get_bookmakers', + 'odds_search_bookmakers', + 'odds_get_markets', + 'odds_search_markets', + 'core_get_continents', + 'core_get_countries', + 'core_search_countries', + 'core_get_regions', + 'core_get_cities', + 'core_search_cities', + 'core_get_types', +] + +export const SportmonksBlock: BlockConfig = { + type: 'sportmonks', + name: 'Sportmonks', + description: 'Access Sportmonks football, motorsport, odds, and reference data', + longDescription: + 'Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, teams, squads, players, standings, and topscorers. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones.', + docsLink: 'https://docs.sim.ai/integrations/sportmonks', + category: 'tools', + integrationType: IntegrationType.Analytics, + bgColor: '#171534', + icon: SportmonksIcon, + authMode: AuthMode.ApiKey, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Football + { label: 'Get Live Football Scores', id: 'football_get_livescores', group: 'Football' }, + { + label: 'Get Inplay Football Scores', + id: 'football_get_inplay_livescores', + group: 'Football', + }, + { + label: 'Get Football Fixtures by Date', + id: 'football_get_fixtures_by_date', + group: 'Football', + }, + { + label: 'Get Football Fixtures by Date Range', + id: 'football_get_fixtures_by_date_range', + group: 'Football', + }, + { label: 'Get Football Fixture by ID', id: 'football_get_fixture', group: 'Football' }, + { label: 'Get Football Head to Head', id: 'football_get_head_to_head', group: 'Football' }, + { label: 'Get Football Leagues', id: 'football_get_leagues', group: 'Football' }, + { label: 'Get Football League by ID', id: 'football_get_league', group: 'Football' }, + { label: 'Search Football Teams', id: 'football_search_teams', group: 'Football' }, + { label: 'Get Football Team by ID', id: 'football_get_team', group: 'Football' }, + { label: 'Get Football Team Squad', id: 'football_get_team_squad', group: 'Football' }, + { label: 'Search Football Players', id: 'football_search_players', group: 'Football' }, + { label: 'Get Football Player by ID', id: 'football_get_player', group: 'Football' }, + { + label: 'Get Football Standings by Season', + id: 'football_get_standings_by_season', + group: 'Football', + }, + { + label: 'Get Football Topscorers by Season', + id: 'football_get_topscorers_by_season', + group: 'Football', + }, + // Motorsport + { + label: 'Get Live Motorsport Scores', + id: 'motorsport_get_livescores', + group: 'Motorsport', + }, + { + label: 'Get Motorsport Fixtures by Date', + id: 'motorsport_get_fixtures_by_date', + group: 'Motorsport', + }, + { + label: 'Get Motorsport Fixture by ID', + id: 'motorsport_get_fixture', + group: 'Motorsport', + }, + { label: 'Get Motorsport Drivers', id: 'motorsport_get_drivers', group: 'Motorsport' }, + { label: 'Get Motorsport Driver by ID', id: 'motorsport_get_driver', group: 'Motorsport' }, + { + label: 'Search Motorsport Drivers', + id: 'motorsport_search_drivers', + group: 'Motorsport', + }, + { label: 'Get Motorsport Teams', id: 'motorsport_get_teams', group: 'Motorsport' }, + { label: 'Get Motorsport Team by ID', id: 'motorsport_get_team', group: 'Motorsport' }, + { + label: 'Get Motorsport Driver Standings by Season', + id: 'motorsport_get_driver_standings_by_season', + group: 'Motorsport', + }, + { + label: 'Get Motorsport Team Standings by Season', + id: 'motorsport_get_team_standings_by_season', + group: 'Motorsport', + }, + { + label: 'Get Motorsport Laps by Fixture', + id: 'motorsport_get_laps_by_fixture', + group: 'Motorsport', + }, + { + label: 'Get Motorsport Pitstops by Fixture', + id: 'motorsport_get_pitstops_by_fixture', + group: 'Motorsport', + }, + // Odds + { + label: 'Get Pre-match Odds by Fixture', + id: 'odds_get_pre_match_odds_by_fixture', + group: 'Odds', + }, + { + label: 'Get In-play Odds by Fixture', + id: 'odds_get_inplay_odds_by_fixture', + group: 'Odds', + }, + { label: 'Get Bookmakers', id: 'odds_get_bookmakers', group: 'Odds' }, + { label: 'Get Bookmaker by ID', id: 'odds_get_bookmaker', group: 'Odds' }, + { label: 'Search Bookmakers', id: 'odds_search_bookmakers', group: 'Odds' }, + { label: 'Get Betting Markets', id: 'odds_get_markets', group: 'Odds' }, + { label: 'Get Betting Market by ID', id: 'odds_get_market', group: 'Odds' }, + { label: 'Search Betting Markets', id: 'odds_search_markets', group: 'Odds' }, + // Core reference data + { label: 'Get Continents', id: 'core_get_continents', group: 'Core (Reference)' }, + { label: 'Get Continent by ID', id: 'core_get_continent', group: 'Core (Reference)' }, + { label: 'Get Countries', id: 'core_get_countries', group: 'Core (Reference)' }, + { label: 'Get Country by ID', id: 'core_get_country', group: 'Core (Reference)' }, + { label: 'Search Countries', id: 'core_search_countries', group: 'Core (Reference)' }, + { label: 'Get Regions', id: 'core_get_regions', group: 'Core (Reference)' }, + { label: 'Get Region by ID', id: 'core_get_region', group: 'Core (Reference)' }, + { label: 'Get Cities', id: 'core_get_cities', group: 'Core (Reference)' }, + { label: 'Get City by ID', id: 'core_get_city', group: 'Core (Reference)' }, + { label: 'Search Cities', id: 'core_search_cities', group: 'Core (Reference)' }, + { label: 'Get Types', id: 'core_get_types', group: 'Core (Reference)' }, + { label: 'Get Type by ID', id: 'core_get_type', group: 'Core (Reference)' }, + { label: 'Get Timezones', id: 'core_get_timezones', group: 'Core (Reference)' }, + ], + value: () => 'football_get_fixtures_by_date', + }, + // Date inputs (football + motorsport fixtures by date) + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['football_get_fixtures_by_date', 'motorsport_get_fixtures_by_date'], + }, + required: { + field: 'operation', + value: ['football_get_fixtures_by_date', 'motorsport_get_fixtures_by_date'], + }, + wandConfig: DATE_WAND_CONFIG, + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, + required: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, + wandConfig: DATE_WAND_CONFIG, + }, + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (max 100 days after start)', + condition: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, + required: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, + wandConfig: DATE_WAND_CONFIG, + }, + // Fixture ID (football + motorsport + odds fixture operations) + { + id: 'fixtureId', + title: 'Fixture ID', + type: 'short-input', + placeholder: 'Numeric fixture ID', + condition: { + field: 'operation', + value: [ + 'football_get_fixture', + 'motorsport_get_fixture', + 'motorsport_get_laps_by_fixture', + 'motorsport_get_pitstops_by_fixture', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_inplay_odds_by_fixture', + ], + }, + required: { + field: 'operation', + value: [ + 'football_get_fixture', + 'motorsport_get_fixture', + 'motorsport_get_laps_by_fixture', + 'motorsport_get_pitstops_by_fixture', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_inplay_odds_by_fixture', + ], + }, + }, + // Head to head team IDs (football) + { + id: 'team1', + title: 'Team 1 ID', + type: 'short-input', + placeholder: 'First team ID', + condition: { field: 'operation', value: 'football_get_head_to_head' }, + required: { field: 'operation', value: 'football_get_head_to_head' }, + }, + { + id: 'team2', + title: 'Team 2 ID', + type: 'short-input', + placeholder: 'Second team ID', + condition: { field: 'operation', value: 'football_get_head_to_head' }, + required: { field: 'operation', value: 'football_get_head_to_head' }, + }, + // League ID (football) + { + id: 'leagueId', + title: 'League ID', + type: 'short-input', + placeholder: 'Numeric league ID', + condition: { field: 'operation', value: 'football_get_league' }, + required: { field: 'operation', value: 'football_get_league' }, + }, + // Team ID (football + motorsport) + { + id: 'teamId', + title: 'Team ID', + type: 'short-input', + placeholder: 'Numeric team ID', + condition: { + field: 'operation', + value: ['football_get_team', 'football_get_team_squad', 'motorsport_get_team'], + }, + required: { + field: 'operation', + value: ['football_get_team', 'football_get_team_squad', 'motorsport_get_team'], + }, + }, + // Driver ID (motorsport) + { + id: 'driverId', + title: 'Driver ID', + type: 'short-input', + placeholder: 'Numeric driver ID', + condition: { field: 'operation', value: 'motorsport_get_driver' }, + required: { field: 'operation', value: 'motorsport_get_driver' }, + }, + // Player ID (football) + { + id: 'playerId', + title: 'Player ID', + type: 'short-input', + placeholder: 'Numeric player ID', + condition: { field: 'operation', value: 'football_get_player' }, + required: { field: 'operation', value: 'football_get_player' }, + }, + // Season ID (football + motorsport standings/topscorers) + { + id: 'seasonId', + title: 'Season ID', + type: 'short-input', + placeholder: 'Numeric season ID', + condition: { + field: 'operation', + value: [ + 'football_get_standings_by_season', + 'football_get_topscorers_by_season', + 'motorsport_get_driver_standings_by_season', + 'motorsport_get_team_standings_by_season', + ], + }, + required: { + field: 'operation', + value: [ + 'football_get_standings_by_season', + 'football_get_topscorers_by_season', + 'motorsport_get_driver_standings_by_season', + 'motorsport_get_team_standings_by_season', + ], + }, + }, + // Bookmaker / Market IDs (odds) + { + id: 'bookmakerId', + title: 'Bookmaker ID', + type: 'short-input', + placeholder: 'Numeric bookmaker ID', + condition: { field: 'operation', value: 'odds_get_bookmaker' }, + required: { field: 'operation', value: 'odds_get_bookmaker' }, + }, + { + id: 'marketId', + title: 'Market ID', + type: 'short-input', + placeholder: 'Numeric market ID', + condition: { field: 'operation', value: 'odds_get_market' }, + required: { field: 'operation', value: 'odds_get_market' }, + }, + // Core reference IDs + { + id: 'continentId', + title: 'Continent ID', + type: 'short-input', + placeholder: 'Numeric continent ID', + condition: { field: 'operation', value: 'core_get_continent' }, + required: { field: 'operation', value: 'core_get_continent' }, + }, + { + id: 'countryId', + title: 'Country ID', + type: 'short-input', + placeholder: 'Numeric country ID', + condition: { field: 'operation', value: 'core_get_country' }, + required: { field: 'operation', value: 'core_get_country' }, + }, + { + id: 'regionId', + title: 'Region ID', + type: 'short-input', + placeholder: 'Numeric region ID', + condition: { field: 'operation', value: 'core_get_region' }, + required: { field: 'operation', value: 'core_get_region' }, + }, + { + id: 'cityId', + title: 'City ID', + type: 'short-input', + placeholder: 'Numeric city ID', + condition: { field: 'operation', value: 'core_get_city' }, + required: { field: 'operation', value: 'core_get_city' }, + }, + { + id: 'typeId', + title: 'Type ID', + type: 'short-input', + placeholder: 'Numeric type ID', + condition: { field: 'operation', value: 'core_get_type' }, + required: { field: 'operation', value: 'core_get_type' }, + }, + // Shared search query + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Name to search for', + condition: { + field: 'operation', + value: [ + 'football_search_teams', + 'football_search_players', + 'motorsport_search_drivers', + 'odds_search_bookmakers', + 'odds_search_markets', + 'core_search_countries', + 'core_search_cities', + ], + }, + required: { + field: 'operation', + value: [ + 'football_search_teams', + 'football_search_players', + 'motorsport_search_drivers', + 'odds_search_bookmakers', + 'odds_search_markets', + 'core_search_countries', + 'core_search_cities', + ], + }, + }, + // Shared enrichment + pagination (advanced) + { + id: 'include', + title: 'Includes', + type: 'short-input', + placeholder: 'Semicolon-separated relations (e.g. participants;scores)', + mode: 'advanced', + condition: { field: 'operation', value: INCLUDE_OPS }, + }, + { + id: 'filters', + title: 'Filters', + type: 'short-input', + placeholder: 'Filters to apply (e.g. fixtureLeagues:501)', + mode: 'advanced', + condition: { field: 'operation', value: FILTER_OPS }, + }, + { + id: 'per_page', + title: 'Per Page', + type: 'short-input', + placeholder: 'Results per page (max 50)', + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPS }, + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: 'Page number', + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPS }, + }, + { + id: 'order', + title: 'Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPS }, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Sportmonks API token', + password: true, + required: true, + }, + ], + tools: { + access: [ + 'sportmonks_football_get_livescores', + 'sportmonks_football_get_inplay_livescores', + 'sportmonks_football_get_fixtures_by_date', + 'sportmonks_football_get_fixtures_by_date_range', + 'sportmonks_football_get_fixture', + 'sportmonks_football_get_head_to_head', + 'sportmonks_football_get_leagues', + 'sportmonks_football_get_league', + 'sportmonks_football_search_teams', + 'sportmonks_football_get_team', + 'sportmonks_football_get_team_squad', + 'sportmonks_football_search_players', + 'sportmonks_football_get_player', + 'sportmonks_football_get_standings_by_season', + 'sportmonks_football_get_topscorers_by_season', + 'sportmonks_motorsport_get_livescores', + 'sportmonks_motorsport_get_fixtures_by_date', + 'sportmonks_motorsport_get_fixture', + 'sportmonks_motorsport_get_drivers', + 'sportmonks_motorsport_get_driver', + 'sportmonks_motorsport_search_drivers', + 'sportmonks_motorsport_get_teams', + 'sportmonks_motorsport_get_team', + 'sportmonks_motorsport_get_driver_standings_by_season', + 'sportmonks_motorsport_get_team_standings_by_season', + 'sportmonks_motorsport_get_laps_by_fixture', + 'sportmonks_motorsport_get_pitstops_by_fixture', + 'sportmonks_odds_get_pre_match_odds_by_fixture', + 'sportmonks_odds_get_inplay_odds_by_fixture', + 'sportmonks_odds_get_bookmakers', + 'sportmonks_odds_get_bookmaker', + 'sportmonks_odds_search_bookmakers', + 'sportmonks_odds_get_markets', + 'sportmonks_odds_get_market', + 'sportmonks_odds_search_markets', + 'sportmonks_core_get_continents', + 'sportmonks_core_get_continent', + 'sportmonks_core_get_countries', + 'sportmonks_core_get_country', + 'sportmonks_core_search_countries', + 'sportmonks_core_get_regions', + 'sportmonks_core_get_region', + 'sportmonks_core_get_cities', + 'sportmonks_core_get_city', + 'sportmonks_core_search_cities', + 'sportmonks_core_get_types', + 'sportmonks_core_get_type', + 'sportmonks_core_get_timezones', + ], + config: { + tool: (params) => `sportmonks_${params.operation}`, + params: (params) => { + const cleaned: Record = {} + Object.entries(params).forEach(([key, value]) => { + if (key === 'operation') return + if (value !== undefined && value !== null && value !== '') { + cleaned[key] = value + } + }) + return cleaned + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Sportmonks API token' }, + date: { type: 'string', description: 'Date in YYYY-MM-DD format' }, + startDate: { type: 'string', description: 'Start date in YYYY-MM-DD format' }, + endDate: { type: 'string', description: 'End date in YYYY-MM-DD format' }, + fixtureId: { type: 'string', description: 'Fixture (session) ID' }, + team1: { type: 'string', description: 'First team ID for head-to-head' }, + team2: { type: 'string', description: 'Second team ID for head-to-head' }, + leagueId: { type: 'string', description: 'League ID' }, + teamId: { type: 'string', description: 'Team ID' }, + driverId: { type: 'string', description: 'Driver ID' }, + playerId: { type: 'string', description: 'Player ID' }, + seasonId: { type: 'string', description: 'Season ID' }, + bookmakerId: { type: 'string', description: 'Bookmaker ID' }, + marketId: { type: 'string', description: 'Market ID' }, + continentId: { type: 'string', description: 'Continent ID' }, + countryId: { type: 'string', description: 'Country ID' }, + regionId: { type: 'string', description: 'Region ID' }, + cityId: { type: 'string', description: 'City ID' }, + typeId: { type: 'string', description: 'Type ID' }, + query: { type: 'string', description: 'Search query' }, + include: { type: 'string', description: 'Semicolon-separated relations to include' }, + filters: { type: 'string', description: 'Filters to apply' }, + per_page: { type: 'string', description: 'Results per page (max 50)' }, + page: { type: 'string', description: 'Page number' }, + order: { type: 'string', description: 'Order direction (asc or desc)' }, + }, + outputs: { + // Football + Motorsport fixtures + fixtures: { + type: 'json', + description: + 'Array of fixtures/sessions [{id, name, starting_at, league_id, season_id, state_id}] — football and motorsport', + }, + fixture: { type: 'json', description: 'Single fixture/session object' }, + // Football + leagues: { type: 'json', description: 'Array of football leagues' }, + league: { type: 'json', description: 'Single football league object' }, + teams: { type: 'json', description: 'Array of teams (football or motorsport)' }, + team: { type: 'json', description: 'Single team object (football or motorsport)' }, + squad: { type: 'json', description: 'Array of football squad entries' }, + players: { type: 'json', description: 'Array of football players' }, + player: { type: 'json', description: 'Single football player object' }, + standings: { + type: 'json', + description: 'Array of standings (football league or motorsport championship)', + }, + topscorers: { type: 'json', description: 'Array of football topscorers' }, + // Motorsport + drivers: { type: 'json', description: 'Array of motorsport drivers' }, + driver: { type: 'json', description: 'Single motorsport driver object' }, + laps: { type: 'json', description: 'Array of motorsport laps' }, + pitstops: { type: 'json', description: 'Array of motorsport pitstops' }, + // Odds + odds: { + type: 'json', + description: + 'Array of odds [{id, fixture_id, market_id, bookmaker_id, label, value, probability}]', + }, + bookmakers: { type: 'json', description: 'Array of bookmakers [{id, name, logo}]' }, + bookmaker: { type: 'json', description: 'Single bookmaker object' }, + markets: { type: 'json', description: 'Array of betting markets [{id, name}]' }, + market: { type: 'json', description: 'Single betting market object' }, + // Core reference + continents: { type: 'json', description: 'Array of continents [{id, name, code}]' }, + continent: { type: 'json', description: 'Single continent object' }, + countries: { + type: 'json', + description: 'Array of countries [{id, name, iso2, iso3, image_path}]', + }, + country: { type: 'json', description: 'Single country object' }, + regions: { type: 'json', description: 'Array of regions [{id, country_id, name}]' }, + region: { type: 'json', description: 'Single region object' }, + cities: { type: 'json', description: 'Array of cities [{id, country_id, name}]' }, + city: { type: 'json', description: 'Single city object' }, + types: { + type: 'json', + description: 'Array of types [{id, name, code, developer_name, group}]', + }, + type: { type: 'json', description: 'Single type object' }, + timezones: { type: 'json', description: 'Array of IANA time zone name strings' }, + pagination: { + type: 'json', + description: 'Pagination metadata {count, per_page, current_page, next_page, has_more}', + }, + }, +} + +export const SportmonksBlockMeta = { + tags: ['data-analytics'], + url: 'https://www.sportmonks.com', + templates: [ + { + icon: SportmonksIcon, + title: 'Daily football fixtures digest', + prompt: + "Build a scheduled daily workflow that fetches today's football fixtures from Sportmonks for the leagues I follow, summarizes the key matchups and kickoff times, and posts the digest to Slack.", + modules: ['scheduled', 'agent', 'workflows'], + category: 'productivity', + tags: ['automation', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: SportmonksIcon, + title: 'Live football score alerter', + prompt: + 'Create a scheduled workflow that polls Sportmonks inplay football scores, detects goals and status changes since the last run, and pings Slack with the updated scoreline for tracked matches.', + modules: ['scheduled', 'tables', 'agent', 'workflows'], + category: 'operations', + tags: ['monitoring', 'automation'], + alsoIntegrations: ['slack'], + }, + { + icon: SportmonksIcon, + title: 'Weekly league standings report', + prompt: + 'Build a scheduled weekly workflow that pulls the Sportmonks football standings and topscorers for a season, formats a league table with recent form, and emails the report to the group.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'operations', + tags: ['reporting', 'analysis'], + alsoIntegrations: ['gmail'], + }, + { + icon: SportmonksIcon, + title: 'Race weekend schedule digest', + prompt: + "Build a scheduled workflow that fetches this weekend's motorsport sessions from Sportmonks, summarizes the practice, qualifying, and race times, and posts the schedule to Slack.", + modules: ['scheduled', 'agent', 'workflows'], + category: 'productivity', + tags: ['automation', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: SportmonksIcon, + title: 'Motorsport championship tracker', + prompt: + 'Create a scheduled weekly workflow that pulls the Sportmonks motorsport driver and constructor standings for the current season, formats the championship tables, and emails them to the group.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'operations', + tags: ['reporting', 'analysis'], + alsoIntegrations: ['gmail'], + }, + { + icon: SportmonksIcon, + title: 'Pre-match odds snapshot', + prompt: + 'Build a workflow that pulls Sportmonks pre-match odds for a fixture across selected bookmakers, computes the implied probability for each outcome, and writes the snapshot to a table.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['finance', 'analysis'], + }, + { + icon: SportmonksIcon, + title: 'Live odds movement alerter', + prompt: + 'Create a scheduled workflow that polls Sportmonks in-play odds for a fixture, detects sharp price moves since the last run, and pings Slack with the updated lines.', + modules: ['scheduled', 'tables', 'agent', 'workflows'], + category: 'operations', + tags: ['monitoring', 'finance'], + alsoIntegrations: ['slack'], + }, + { + icon: SportmonksIcon, + title: 'Head-to-head match preview', + prompt: + 'Create a workflow that takes two team names, resolves them to IDs via Sportmonks football team search, pulls their head-to-head history and current standings, and writes a match preview file.', + modules: ['agent', 'files', 'workflows'], + category: 'productivity', + tags: ['research', 'content'], + }, + ], + skills: [ + { + name: 'daily-football-fixtures', + description: "List a day's football fixtures from Sportmonks, optionally filtered by league.", + content: + '# Daily Football Fixtures\n\nGet the football matches scheduled for a given day.\n\n## Steps\n1. Use Get Football Fixtures by Date with the target date in YYYY-MM-DD format.\n2. Optionally set Includes to `participants;scores;league` to enrich each fixture.\n3. Optionally set Filters such as `fixtureLeagues:501,271` to restrict to specific leagues.\n\n## Output\nA list of fixtures with kickoff time, the participating teams, and league.', + }, + { + name: 'live-football-scores', + description: 'Fetch in-play football matches and their current scores from Sportmonks.', + content: + '# Live Football Scores\n\nSee which matches are being played now and the live score.\n\n## Steps\n1. Use Get Inplay Football Scores to fetch matches in progress.\n2. Set Includes to `participants;scores` for team names and the scoreline.\n3. Optionally filter with `fixtureLeagues:501`.\n\n## Output\nA list of live fixtures, each with the two teams, current score, and match state.', + }, + { + name: 'football-league-table', + description: 'Build a football league standings table for a season from Sportmonks.', + content: + "# Football League Table\n\nGet the current standings for a competition.\n\n## Steps\n1. Find the season ID (use Get Football Leagues, then its current season).\n2. Use Get Football Standings by Season with that season ID.\n3. Set Includes to `participant` for team names and `form` for recent results.\n\n## Output\nAn ordered league table with each team's position, points, and recent form.", + }, + { + name: 'race-weekend-sessions', + description: 'List the motorsport sessions on a given date from Sportmonks.', + content: + '# Race Weekend Sessions\n\nGet the practice, qualifying, and race sessions for a day.\n\n## Steps\n1. Use Get Motorsport Fixtures by Date with the target date in YYYY-MM-DD format.\n2. Set Includes to `venue;participants` to attach the track and entrants.\n\n## Output\nA list of sessions for the day with type (Practice/Qualifying/Race), track, and start time.', + }, + { + name: 'motorsport-championship', + description: 'Fetch driver and constructor championship standings for a motorsport season.', + content: + "# Motorsport Championship\n\nGet the title race state for a season.\n\n## Steps\n1. Use Get Motorsport Driver Standings by Season with the season ID.\n2. Use Get Motorsport Team Standings by Season with the same season ID.\n3. Set Includes to `participant` for driver/team names.\n\n## Output\nOrdered drivers and constructors tables with each participant's position and points.", + }, + { + name: 'pre-match-odds', + description: 'Fetch pre-match odds for a fixture and compute implied probabilities.', + content: + '# Pre-match Odds\n\nGet the betting odds for an upcoming fixture.\n\n## Steps\n1. Use Get Pre-match Odds by Fixture with the fixture ID.\n2. Set Includes to `market;bookmaker` and optionally Filters like `bookmakers:2,14` to narrow results.\n\n## Output\nThe odds per outcome with decimal value and implied probability, grouped by market and bookmaker.', + }, + { + name: 'odds-line-shopping', + description: 'Find the best available price per outcome across bookmakers.', + content: + '# Odds Line Shopping\n\nFind the best odds for each outcome.\n\n## Steps\n1. Use Get Pre-match Odds by Fixture for the fixture (do not filter to one bookmaker).\n2. Group the returned odds by market and outcome label.\n3. For each outcome, pick the highest value and note its bookmaker_id.\n\n## Output\nFor each outcome, the best decimal price and which bookmaker offers it.', + }, + { + name: 'resolve-country', + description: + 'Resolve a country name to its Sportmonks ID and ISO codes via Core reference data.', + content: + '# Resolve Country\n\nNormalize a country name to Sportmonks reference data.\n\n## Steps\n1. Use Search Countries with the country name.\n2. Read the matching country id, iso2, iso3, and fifa_name.\n\n## Output\nThe country id plus its ISO2, ISO3, and FIFA codes for use in other lookups.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 96d2f7f2f6e..a03be207968 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -271,6 +271,7 @@ import { SimilarwebBlock, SimilarwebBlockMeta } from '@/blocks/blocks/similarweb import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour' import { SlackBlock, SlackBlockMeta } from '@/blocks/blocks/slack' import { SmtpBlock } from '@/blocks/blocks/smtp' +import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks' import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify' import { SQSBlock, SQSBlockMeta } from '@/blocks/blocks/sqs' import { SquareBlock, SquareBlockMeta } from '@/blocks/blocks/square' @@ -578,6 +579,7 @@ const BLOCK_REGISTRY: Record = { sixtyfour: SixtyfourBlock, slack: SlackBlock, smtp: SmtpBlock, + sportmonks: SportmonksBlock, spotify: SpotifyBlock, sqs: SQSBlock, square: SquareBlock, @@ -842,6 +844,7 @@ const BLOCK_META_REGISTRY: Record = { similarweb: SimilarwebBlockMeta, sixtyfour: SixtyfourBlockMeta, slack: SlackBlockMeta, + sportmonks: SportmonksBlockMeta, spotify: SpotifyBlockMeta, sqs: SQSBlockMeta, square: SquareBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 89f0b818ddc..c920a30428c 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1958,6 +1958,24 @@ export function WhatsAppIcon(props: SVGProps) { ) } +export function SportmonksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function SquareIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/copilot/chat/display-message.ts b/apps/sim/lib/copilot/chat/display-message.ts index 7b2dffa3783..ac7ba92e790 100644 --- a/apps/sim/lib/copilot/chat/display-message.ts +++ b/apps/sim/lib/copilot/chat/display-message.ts @@ -121,6 +121,46 @@ function toDisplayContexts( })) } +const WORKSPACE_FILE_TOOL = 'workspace_file' +const EDIT_CONTENT_TOOL = 'edit_content' +const MAIN_SPAN = 'main' + +/** + * Collapses an `edit_content` write into the most-recent `workspace_file` row in + * the same subagent span, mirroring the live turn-model fold. The live view + * folds these in `reduceEvent`, but the persisted transcript stores them as two + * separate tool blocks; without this a reloaded chat splits the file write into + * "workspace_file" + "edit_content" rows (and a refresh mid-write leaves the + * second row spinning). The reopened row inherits the edit_content's final + * status/result, exactly as the live single "writing" row resolves. Every other + * block is passed through untouched, so this only affects file writes. + */ +function foldFileWriteBlocks(blocks: ContentBlock[]): ContentBlock[] { + const folded: ContentBlock[] = [] + const workspaceFileIndexBySpan = new Map() + for (const block of blocks) { + const tc = block.type === ContentBlockType.tool_call ? block.toolCall : undefined + if (tc) { + const span = block.spanId ?? MAIN_SPAN + if (tc.name === EDIT_CONTENT_TOOL) { + const parentIndex = workspaceFileIndexBySpan.get(span) + const parent = parentIndex !== undefined ? folded[parentIndex] : undefined + if (parent?.type === ContentBlockType.tool_call && parent.toolCall) { + folded[parentIndex!] = { + ...parent, + toolCall: { ...parent.toolCall, status: tc.status, result: tc.result }, + } + continue + } + } else if (tc.name === WORKSPACE_FILE_TOOL) { + workspaceFileIndexBySpan.set(span, folded.length) + } + } + folded.push(block) + } + return folded +} + const displayMessageCache = new WeakMap() /** @@ -144,9 +184,10 @@ export function toDisplayMessage(msg: PersistedMessage): ChatMessage { } if (msg.contentBlocks && msg.contentBlocks.length > 0) { - display.contentBlocks = msg.contentBlocks + const displayBlocks = msg.contentBlocks .map(toDisplayBlock) .filter((block): block is ContentBlock => !!block) + display.contentBlocks = foldFileWriteBlocks(displayBlocks) } const attachments = toDisplayAttachment(msg.fileAttachments) diff --git a/apps/sim/lib/copilot/chat/effective-transcript.ts b/apps/sim/lib/copilot/chat/effective-transcript.ts index cd5f432b142..f615448a356 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.ts @@ -119,6 +119,9 @@ function buildLiveAssistantMessage(params: { let requestId: string | undefined let lastTimestamp: string | undefined + // Scope-only resolution (mirrors the live browser stream loop): with + // concurrent subagents the legacy activeSubagent fallback / name-match scan + // would mis-attribute interleaved replayed events to the wrong lane. const resolveScopedSubagent = ( agentId: string | undefined, parentToolCallId: string | undefined, @@ -133,7 +136,7 @@ function buildLiveAssistantMessage(params: { const scoped = subagentByParentToolCallId.get(parentToolCallId) if (scoped) return scoped } - return activeSubagent + return undefined } const resolveParentForSubagentBlock = ( @@ -141,12 +144,7 @@ function buildLiveAssistantMessage(params: { scopedParent: string | undefined ): string | undefined => { if (!subagent) return undefined - if (scopedParent) return scopedParent - if (activeSubagent === subagent) return activeSubagentParentToolCallId - for (const [parent, name] of subagentByParentToolCallId) { - if (name === subagent) return parent - } - return undefined + return scopedParent } const ensureToolBlock = (input: { @@ -364,11 +362,10 @@ function buildLiveAssistantMessage(params: { if (parentToolCallId) { subagentByParentToolCallId.delete(parentToolCallId) } - if ( - !parentToolCallId || - parentToolCallId === activeSubagentParentToolCallId || - name === activeSubagent - ) { + // Clear the legacy pointer only for THIS lane (by parent tool call id) + // or an unscoped end — never by agent name, which would tear down a + // concurrent same-name sibling that is still open. + if (!parentToolCallId || parentToolCallId === activeSubagentParentToolCallId) { activeSubagent = undefined activeSubagentParentToolCallId = undefined } diff --git a/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts b/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts index 88e8344e560..91059cbbd2f 100644 --- a/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts +++ b/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts @@ -50,6 +50,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = { MothershipStreamV1CheckpointPauseFrame: { additionalProperties: false, properties: { + checkpointId: { + type: 'string', + }, parentToolCallId: { type: 'string', }, diff --git a/apps/sim/lib/copilot/generated/mothership-stream-v1.ts b/apps/sim/lib/copilot/generated/mothership-stream-v1.ts index d7194f13757..32ca1d88d51 100644 --- a/apps/sim/lib/copilot/generated/mothership-stream-v1.ts +++ b/apps/sim/lib/copilot/generated/mothership-stream-v1.ts @@ -319,6 +319,7 @@ export interface MothershipStreamV1CheckpointPausePayload { runId: string } export interface MothershipStreamV1CheckpointPauseFrame { + checkpointId?: string parentToolCallId: string parentToolName: string pendingToolIds: string[] diff --git a/apps/sim/lib/copilot/request/context/request-context.ts b/apps/sim/lib/copilot/request/context/request-context.ts index 9ee241ba183..a38e68a35d1 100644 --- a/apps/sim/lib/copilot/request/context/request-context.ts +++ b/apps/sim/lib/copilot/request/context/request-context.ts @@ -18,17 +18,15 @@ export function createStreamingContext(overrides?: Partial): S toolCalls: new Map(), pendingToolPromises: new Map(), currentThinkingBlock: null, - currentSubagentThinkingBlock: null, + subagentThinkingBlocks: new Map(), isInThinkingBlock: false, - subAgentParentToolCallId: undefined, - subAgentParentStack: [], subAgentContent: {}, subAgentToolCalls: {}, pendingContent: '', streamComplete: false, wasAborted: false, errors: [], - activeFileIntent: null, + activeFileIntents: new Map(), trace: new TraceCollector(), ...overrides, } diff --git a/apps/sim/lib/copilot/request/context/result.test.ts b/apps/sim/lib/copilot/request/context/result.test.ts index 1cbbd5a5103..3ea2b984ad2 100644 --- a/apps/sim/lib/copilot/request/context/result.test.ts +++ b/apps/sim/lib/copilot/request/context/result.test.ts @@ -23,9 +23,8 @@ function makeContext(): StreamingContext { pendingToolPromises: new Map(), awaitingAsyncContinuation: undefined, currentThinkingBlock: null, + subagentThinkingBlocks: new Map(), isInThinkingBlock: false, - subAgentParentToolCallId: undefined, - subAgentParentStack: [], subAgentContent: {}, subAgentToolCalls: {}, pendingContent: '', diff --git a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts index 46c40413c01..581dd8c8f93 100644 --- a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts +++ b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts @@ -13,15 +13,13 @@ import { upsertFilePreviewSession, } from '@/lib/copilot/request/session' import type { + ActiveFileIntent, ExecutionContext, OrchestratorOptions, StreamEvent, StreamingContext, } from '@/lib/copilot/request/types' -import { - clearIntentsForWorkspace, - peekFileIntent, -} from '@/lib/copilot/tools/server/files/file-intent-store' +import { peekFileIntent } from '@/lib/copilot/tools/server/files/file-intent-store' import { buildFilePreviewText, loadWorkspaceFileTextForPreview, @@ -31,7 +29,7 @@ import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/ const logger = createLogger('CopilotFilePreviewAdapter') type JsonRecord = Record -type FileIntent = NonNullable +type FileIntent = ActiveFileIntent type EditContentStreamState = { raw: string @@ -348,6 +346,19 @@ export async function processFilePreviewStreamEvent(input: { const { streamId, streamEvent, context, execContext, options, state } = input const { editContentState, filePreviewState } = state + // Scope the in-flight intent to the invoking file subagent's channel (its + // outer tool_use id) so two file agents streaming concurrently never read or + // overwrite each other's intent. workspace_file and edit_content from the same + // file agent share this channel id, so they pair up; siblings stay isolated. + const channelId = streamEvent.scope?.parentToolCallId ?? '' + const getIntent = (): FileIntent | null => context.activeFileIntents.get(channelId) ?? null + const setIntent = (intent: FileIntent): void => { + context.activeFileIntents.set(channelId, intent) + } + const clearIntent = (): void => { + context.activeFileIntents.delete(channelId) + } + if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'workspace_file') { const toolCallId = streamEvent.payload.toolCallId const parsedArgs = parseWorkspaceFileArgs(streamEvent.payload.arguments) @@ -361,31 +372,10 @@ export async function processFilePreviewStreamEvent(input: { const { fileId, fileName } = target const isContentOp = isContentOperation(operation) - if (context.activeFileIntent && isContentOp) { - logger.warn( - 'Orphaned workspace_file intent: content-op workspace_file arrived without edit_content for prior intent', - { - orphanedToolCallId: context.activeFileIntent.toolCallId, - orphanedOperation: context.activeFileIntent.operation, - newToolCallId: toolCallId, - newOperation: operation, - } - ) - if (execContext.workspaceId) { - const cleared = await clearIntentsForWorkspace(execContext.workspaceId, { - chatId: execContext.chatId, - messageId: execContext.messageId, - }) - if (cleared > 0) { - logger.warn('Cleared orphaned execution intents from store', { - cleared, - workspaceId: execContext.workspaceId, - }) - } - } - } - - context.activeFileIntent = { + // Per-channel: a re-declared workspace_file just overwrites THIS channel's + // slot. No cross-message intent clearing — that would wipe a concurrent + // sibling file agent's pending intent. + const intent: FileIntent = { toolCallId, operation, target, @@ -393,6 +383,7 @@ export async function processFilePreviewStreamEvent(input: { ...(contentType ? { contentType } : {}), ...(edit ? { edit } : {}), } + setIntent(intent) if (isContentOp && previewTargetKind) { let previewBaseContent: string | undefined @@ -407,7 +398,7 @@ export async function processFilePreviewStreamEvent(input: { ) } - let session = buildPreviewSessionFromIntent(streamId, context.activeFileIntent) + let session = buildPreviewSessionFromIntent(streamId, intent) if (previewBaseContent !== undefined) { session = { ...session, baseContent: previewBaseContent } } @@ -447,29 +438,30 @@ export async function processFilePreviewStreamEvent(input: { } } + const workspaceResultIntent = getIntent() if ( isToolResultStreamEvent(streamEvent) && streamEvent.payload.toolName === 'workspace_file' && - context.activeFileIntent && - isContentOperation(context.activeFileIntent.operation) + workspaceResultIntent && + isContentOperation(workspaceResultIntent.operation) ) { const result = extractWorkspaceFileResult(streamEvent.payload.output) - if (result.fileId && context.activeFileIntent.target.kind === 'path') { - context.activeFileIntent = { - ...context.activeFileIntent, + if (result.fileId && workspaceResultIntent.target.kind === 'path') { + const intent: FileIntent = { + ...workspaceResultIntent, target: { kind: 'file_id', fileId: result.fileId, - fileName: result.fileName ?? context.activeFileIntent.target.fileName, - path: context.activeFileIntent.target.path, + fileName: result.fileName ?? workspaceResultIntent.target.fileName, + path: workspaceResultIntent.target.path, }, } + setIntent(intent) let previewBaseContent: string | undefined if ( execContext.workspaceId && - (context.activeFileIntent.operation === 'append' || - context.activeFileIntent.operation === 'patch') + (intent.operation === 'append' || intent.operation === 'patch') ) { previewBaseContent = await loadWorkspaceFileTextForPreview( execContext.workspaceId, @@ -477,11 +469,11 @@ export async function processFilePreviewStreamEvent(input: { ) } - let session = buildPreviewSessionFromIntent(streamId, context.activeFileIntent) + let session = buildPreviewSessionFromIntent(streamId, intent) if (previewBaseContent !== undefined) { session = { ...session, baseContent: previewBaseContent } } - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(intent.toolCallId, { session, lastEmittedPreviewText: '', lastSnapshotAt: 0, @@ -489,46 +481,47 @@ export async function processFilePreviewStreamEvent(input: { await persistFilePreviewSession(session) await emitPreviewEvent(streamEvent, options, { - toolCallId: context.activeFileIntent.toolCallId, + toolCallId: intent.toolCallId, toolName: 'workspace_file', previewPhase: 'file_preview_start', }) await emitPreviewEvent(streamEvent, options, { - toolCallId: context.activeFileIntent.toolCallId, + toolCallId: intent.toolCallId, toolName: 'workspace_file', previewPhase: 'file_preview_target', - operation: context.activeFileIntent.operation, + operation: intent.operation, target: { kind: 'file_id', fileId: result.fileId, ...(result.fileName ? { fileName: result.fileName } : {}), }, - ...(context.activeFileIntent.title ? { title: context.activeFileIntent.title } : {}), + ...(intent.title ? { title: intent.title } : {}), }) - if (context.activeFileIntent.edit) { + if (intent.edit) { await emitPreviewEvent(streamEvent, options, { - toolCallId: context.activeFileIntent.toolCallId, + toolCallId: intent.toolCallId, toolName: 'workspace_file', previewPhase: 'file_preview_edit_meta', - edit: context.activeFileIntent.edit, + edit: intent.edit, }) } } } + const patchDeleteIntent = getIntent() if ( isToolResultStreamEvent(streamEvent) && streamEvent.payload.toolName === 'workspace_file' && - context.activeFileIntent && - isContentOperation(context.activeFileIntent.operation) && - context.activeFileIntent.operation === 'patch' && - context.activeFileIntent.edit?.strategy === 'anchored' && - context.activeFileIntent.edit?.mode === 'delete_between' && + patchDeleteIntent && + isContentOperation(patchDeleteIntent.operation) && + patchDeleteIntent.operation === 'patch' && + patchDeleteIntent.edit?.strategy === 'anchored' && + patchDeleteIntent.edit?.mode === 'delete_between' && execContext.workspaceId && - context.activeFileIntent.target.fileId && - !isDocFormat(context.activeFileIntent.target.fileName) + patchDeleteIntent.target.fileId && + !isDocFormat(patchDeleteIntent.target.fileName) ) { - const currentPreview = filePreviewState.get(context.activeFileIntent.toolCallId) + const currentPreview = filePreviewState.get(patchDeleteIntent.toolCallId) const previewText = buildFilePreviewText({ operation: 'patch', streamedContent: '', @@ -539,7 +532,7 @@ export async function processFilePreviewStreamEvent(input: { if (previewText !== undefined) { const baseSession = buildPreviewSessionFromIntent( streamId, - context.activeFileIntent, + patchDeleteIntent, currentPreview?.session ) const nextSession: FilePreviewSession = { @@ -549,7 +542,7 @@ export async function processFilePreviewStreamEvent(input: { previewVersion: (currentPreview?.session.previewVersion ?? 0) + 1, updatedAt: new Date().toISOString(), } - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(patchDeleteIntent.toolCallId, { session: nextSession, lastEmittedPreviewText: previewText, lastSnapshotAt: Date.now(), @@ -577,29 +570,30 @@ export async function processFilePreviewStreamEvent(input: { const stateForTool = editContentState.get(toolCallId) ?? { raw: '' } stateForTool.raw += delta - if (context.activeFileIntent) { + const editIntent = getIntent() + if (editIntent) { const streamedContent = extractEditContent(stateForTool.raw) if (streamedContent !== (stateForTool.lastContentSnapshot ?? '')) { stateForTool.lastContentSnapshot = streamedContent - let currentPreview = filePreviewState.get(context.activeFileIntent.toolCallId) ?? { - session: buildPreviewSessionFromIntent(streamId, context.activeFileIntent), + let currentPreview = filePreviewState.get(editIntent.toolCallId) ?? { + session: buildPreviewSessionFromIntent(streamId, editIntent), lastEmittedPreviewText: '', lastSnapshotAt: 0, } if ( currentPreview.session.baseContent === undefined && - (context.activeFileIntent.operation === 'append' || - context.activeFileIntent.operation === 'patch') && + (editIntent.operation === 'append' || editIntent.operation === 'patch') && execContext.workspaceId && - context.activeFileIntent.target.fileId + editIntent.target.fileId ) { const intentBase = await peekFileIntent( execContext.workspaceId, - context.activeFileIntent.target.fileId, + editIntent.target.fileId, { chatId: execContext.chatId, messageId: execContext.messageId, + channelId, } ) if (typeof intentBase?.existingContent === 'string') { @@ -612,14 +606,14 @@ export async function processFilePreviewStreamEvent(input: { ...currentPreview, session: seededSession, } - filePreviewState.set(context.activeFileIntent.toolCallId, currentPreview) + filePreviewState.set(editIntent.toolCallId, currentPreview) await persistFilePreviewSession(seededSession) } } - const previewText = isContentOperation(context.activeFileIntent.operation) + const previewText = isContentOperation(editIntent.operation) ? buildFilePreviewText({ - operation: context.activeFileIntent.operation, + operation: editIntent.operation, streamedContent, existingContent: currentPreview.session.baseContent, edit: currentPreview.session.edit, @@ -629,7 +623,7 @@ export async function processFilePreviewStreamEvent(input: { if (previewText !== undefined) { const baseSession = buildPreviewSessionFromIntent( streamId, - context.activeFileIntent, + editIntent, currentPreview.session ) const now = Date.now() @@ -647,7 +641,7 @@ export async function processFilePreviewStreamEvent(input: { nextSession.operation === 'patch' && now - currentPreview.lastSnapshotAt < PATCH_PREVIEW_SNAPSHOT_INTERVAL_MS ) { - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(editIntent.toolCallId, { session: nextSession, lastEmittedPreviewText: currentPreview.lastEmittedPreviewText, lastSnapshotAt: currentPreview.lastSnapshotAt, @@ -661,7 +655,7 @@ export async function processFilePreviewStreamEvent(input: { nextSession.operation ) - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(editIntent.toolCallId, { session: nextSession, lastEmittedPreviewText: nextSession.previewText, lastSnapshotAt: previewUpdate.lastSnapshotAt, @@ -682,7 +676,7 @@ export async function processFilePreviewStreamEvent(input: { }) } } else { - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(editIntent.toolCallId, { session: currentPreview.session, lastEmittedPreviewText: currentPreview.lastEmittedPreviewText, lastSnapshotAt: currentPreview.lastSnapshotAt, @@ -701,12 +695,13 @@ export async function processFilePreviewStreamEvent(input: { } } + const editResultIntent = getIntent() if ( isToolResultStreamEvent(streamEvent) && streamEvent.payload.toolName === 'edit_content' && - context.activeFileIntent + editResultIntent ) { - const currentPreview = filePreviewState.get(context.activeFileIntent.toolCallId) + const currentPreview = filePreviewState.get(editResultIntent.toolCallId) const completedAt = new Date().toISOString() if ( @@ -714,7 +709,7 @@ export async function processFilePreviewStreamEvent(input: { currentPreview.lastEmittedPreviewText !== currentPreview.session.previewText && currentPreview.session.previewText.length > 0 ) { - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(editResultIntent.toolCallId, { session: currentPreview.session, lastEmittedPreviewText: currentPreview.session.previewText, lastSnapshotAt: Date.now(), @@ -745,7 +740,7 @@ export async function processFilePreviewStreamEvent(input: { updatedAt: completedAt, completedAt, } - filePreviewState.set(context.activeFileIntent.toolCallId, { + filePreviewState.set(editResultIntent.toolCallId, { session: completedSession, lastEmittedPreviewText: completedSession.previewText, lastSnapshotAt: Date.now(), @@ -754,13 +749,13 @@ export async function processFilePreviewStreamEvent(input: { } await emitPreviewEvent(streamEvent, options, { - toolCallId: context.activeFileIntent.toolCallId, + toolCallId: editResultIntent.toolCallId, toolName: 'workspace_file', previewPhase: 'file_preview_complete', - fileId: context.activeFileIntent.target.fileId, + fileId: editResultIntent.target.fileId, output: streamEvent.payload.output, ...(currentPreview ? { previewVersion: currentPreview.session.previewVersion } : {}), }) - context.activeFileIntent = null + clearIntent() } } diff --git a/apps/sim/lib/copilot/request/go/stream.test.ts b/apps/sim/lib/copilot/request/go/stream.test.ts index a152fad1b9d..396615728c2 100644 --- a/apps/sim/lib/copilot/request/go/stream.test.ts +++ b/apps/sim/lib/copilot/request/go/stream.test.ts @@ -94,17 +94,15 @@ function createStreamingContext(): StreamingContext { toolCalls: new Map(), pendingToolPromises: new Map(), currentThinkingBlock: null, - currentSubagentThinkingBlock: null, + subagentThinkingBlocks: new Map(), isInThinkingBlock: false, - subAgentParentToolCallId: undefined, - subAgentParentStack: [], subAgentContent: {}, subAgentToolCalls: {}, pendingContent: '', streamComplete: false, wasAborted: false, errors: [], - activeFileIntent: null, + activeFileIntents: new Map(), trace: new TraceCollector(), } } diff --git a/apps/sim/lib/copilot/request/go/stream.ts b/apps/sim/lib/copilot/request/go/stream.ts index 362da84bb08..880e6483aa8 100644 --- a/apps/sim/lib/copilot/request/go/stream.ts +++ b/apps/sim/lib/copilot/request/go/stream.ts @@ -392,10 +392,6 @@ export async function runStreamLoop( flushThinkingBlock(context) if (spanEvt === MothershipStreamV1SpanLifecycleEvent.start) { if (toolCallId) { - if (!context.subAgentParentStack.includes(toolCallId)) { - context.subAgentParentStack.push(toolCallId) - } - context.subAgentParentToolCallId = toolCallId context.subAgentContent[toolCallId] ??= '' context.subAgentToolCalls[toolCallId] ??= [] } @@ -424,20 +420,9 @@ export async function runStreamLoop( if (isPendingPause) { return } - if (toolCallId) { - const idx = context.subAgentParentStack.lastIndexOf(toolCallId) - if (idx >= 0) { - context.subAgentParentStack.splice(idx, 1) - } else { - logger.warn('subagent end without matching start', { toolCallId }) - } - } else { + if (!toolCallId) { logger.warn('subagent end missing toolCallId') } - context.subAgentParentToolCallId = - context.subAgentParentStack.length > 0 - ? context.subAgentParentStack[context.subAgentParentStack.length - 1] - : undefined if (toolCallId) { for (let i = context.contentBlocks.length - 1; i >= 0; i--) { const b = context.contentBlocks[i] @@ -456,10 +441,18 @@ export async function runStreamLoop( } } - if (handleSubagentRouting(streamEvent, context)) { - const handler = subAgentHandlers[streamEvent.type] - if (handler) { - await handler(streamEvent, context, execContext, options) + // Subagent-lane events are routed ONLY by their own scope. A valid one + // (has parentToolCallId) goes to the subagent handler; a malformed one + // (missing parentToolCallId — Go always stamps it, so this is defensive) + // is DROPPED rather than falling through to the main handler, which would + // merge foreign subagent text/tools into the durable main assistant + // message and mis-attribute it. + if (streamEvent.scope?.lane === 'subagent') { + if (handleSubagentRouting(streamEvent, context)) { + const handler = subAgentHandlers[streamEvent.type] + if (handler) { + await handler(streamEvent, context, execContext, options) + } } return context.streamComplete || undefined } diff --git a/apps/sim/lib/copilot/request/handlers/handlers.test.ts b/apps/sim/lib/copilot/request/handlers/handlers.test.ts index d55fe63bbaa..8d4cb83c669 100644 --- a/apps/sim/lib/copilot/request/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/request/handlers/handlers.test.ts @@ -91,9 +91,8 @@ describe('sse-handlers tool lifecycle', () => { toolCalls: new Map(), pendingToolPromises: new Map(), currentThinkingBlock: null, + subagentThinkingBlocks: new Map(), isInThinkingBlock: false, - subAgentParentToolCallId: undefined, - subAgentParentStack: [], subAgentContent: {}, subAgentToolCalls: {}, pendingContent: '', @@ -461,8 +460,6 @@ describe('sse-handlers tool lifecycle', () => { it('updates stored params when a subagent generating event is followed by the final tool call', async () => { executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } }) - context.subAgentParentToolCallId = 'parent-1' - context.subAgentParentStack = ['parent-1'] context.toolCalls.set('parent-1', { id: 'parent-1', name: 'workflow', @@ -521,7 +518,6 @@ describe('sse-handlers tool lifecycle', () => { }) it('routes subagent text using the event scope parent tool call id', async () => { - context.subAgentParentToolCallId = 'wrong-parent' context.subAgentContent['parent-1'] = '' await subAgentHandlers.text( @@ -572,7 +568,6 @@ describe('sse-handlers tool lifecycle', () => { it('routes subagent tool calls using the event scope parent tool call id', async () => { executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } }) - context.subAgentParentToolCallId = 'wrong-parent' context.toolCalls.set('parent-1', { id: 'parent-1', name: 'deploy', @@ -603,6 +598,65 @@ describe('sse-handlers tool lifecycle', () => { expect(context.subAgentToolCalls['parent-1']?.[0]?.id).toBe('sub-tool-scope-1') }) + it('keeps two concurrent subagent lanes separate for text and thinking', async () => { + const send = (parent: string, channel: MothershipStreamV1TextChannel, text: string) => + subAgentHandlers.text( + { + type: MothershipStreamV1EventType.text, + scope: { + lane: 'subagent', + parentToolCallId: parent, + spanId: `span-${parent}`, + agentId: 'research', + }, + payload: { channel, text }, + } satisfies StreamEvent, + context, + execContext, + { interactive: false, timeout: 1000 } + ) + + // Interleaved thinking across two concurrent lanes. + await send('A', MothershipStreamV1TextChannel.thinking, 'A-think-1 ') + await send('B', MothershipStreamV1TextChannel.thinking, 'B-think-1 ') + await send('A', MothershipStreamV1TextChannel.thinking, 'A-think-2') + + // Each lane accumulates its own thinking block — no cross-contamination. + expect(context.subagentThinkingBlocks.get('A')?.content).toBe('A-think-1 A-think-2') + expect(context.subagentThinkingBlocks.get('B')?.content).toBe('B-think-1 ') + + // Interleaved assistant text across the two lanes. + await send('A', MothershipStreamV1TextChannel.assistant, 'A-text') + await send('B', MothershipStreamV1TextChannel.assistant, 'B-text') + + expect(context.subAgentContent.A).toBe('A-text') + expect(context.subAgentContent.B).toBe('B-text') + + // Assistant text flushed each lane's thinking into contentBlocks, attributed + // to the correct parent (not whichever subagent streamed most recently). + const thinking = context.contentBlocks.filter((b) => b.type === 'subagent_thinking') + expect(thinking.find((b) => b.parentToolCallId === 'A')?.content).toBe('A-think-1 A-think-2') + expect(thinking.find((b) => b.parentToolCallId === 'B')?.content).toBe('B-think-1 ') + }) + + it('drops a subagent text event that is missing its parent tool call id', async () => { + const before = context.contentBlocks.length + await subAgentHandlers.text( + { + type: MothershipStreamV1EventType.text, + scope: { lane: 'subagent', agentId: 'research' }, + payload: { channel: MothershipStreamV1TextChannel.assistant, text: 'orphan' }, + } satisfies StreamEvent, + context, + execContext, + { interactive: false, timeout: 1000 } + ) + + // No lane to attribute to — nothing is added rather than mis-attributed. + expect(context.contentBlocks.length).toBe(before) + expect(Object.keys(context.subAgentContent)).not.toContain('undefined') + }) + it('skips duplicate tool_call after result', async () => { executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } }) diff --git a/apps/sim/lib/copilot/request/handlers/index.ts b/apps/sim/lib/copilot/request/handlers/index.ts index 8231775914a..b170f9104b8 100644 --- a/apps/sim/lib/copilot/request/handlers/index.ts +++ b/apps/sim/lib/copilot/request/handlers/index.ts @@ -33,17 +33,16 @@ export const subAgentHandlers: Record = { [MothershipStreamV1EventType.span]: handleSpanEvent, } -export function handleSubagentRouting(event: StreamEvent, context: StreamingContext): boolean { +export function handleSubagentRouting(event: StreamEvent, _context: StreamingContext): boolean { if (event.scope?.lane !== 'subagent') return false - // Keep the latest scoped parent on hand for legacy callers, but subagent - // handlers should prefer the event-local scope for correctness. - if (event.scope?.parentToolCallId) { - context.subAgentParentToolCallId = event.scope.parentToolCallId - } - - if (!context.subAgentParentToolCallId) { - logger.warn('Subagent event missing parent tool call', { + // Scope-only attribution: a subagent event MUST carry its own parentToolCallId. + // With concurrent subagents there is no single "current" lane to fall back to — + // routing by a global pointer would mis-attribute interleaved events to the + // last-started subagent. A missing parentToolCallId is a contract violation + // (Go always stamps it), so warn and route to the main lane rather than guess. + if (!event.scope?.parentToolCallId) { + logger.warn('Subagent event missing parent tool call id; routing to main lane', { type: event.type, subagent: event.scope?.agentId, }) diff --git a/apps/sim/lib/copilot/request/handlers/run.ts b/apps/sim/lib/copilot/request/handlers/run.ts index 830d935e4cc..593eecce536 100644 --- a/apps/sim/lib/copilot/request/handlers/run.ts +++ b/apps/sim/lib/copilot/request/handlers/run.ts @@ -18,6 +18,9 @@ export const handleRunEvent: StreamHandler = (event, context) => { parentToolCallId: frame.parentToolCallId, parentToolName: frame.parentToolName, pendingToolIds: frame.pendingToolIds, + // Carried through for the per-subagent resume fan-out; undefined under the + // legacy bundled-frame model (all frames share the top-level checkpointId). + ...(frame.checkpointId ? { checkpointId: frame.checkpointId } : {}), })) context.awaitingAsyncContinuation = { diff --git a/apps/sim/lib/copilot/request/handlers/span.ts b/apps/sim/lib/copilot/request/handlers/span.ts index 978e6ec0780..2ad6dcf3382 100644 --- a/apps/sim/lib/copilot/request/handlers/span.ts +++ b/apps/sim/lib/copilot/request/handlers/span.ts @@ -30,19 +30,23 @@ export const handleSpanEvent: StreamHandler = (event, context) => { if (kind === MothershipStreamV1SpanPayloadKind.subagent) { const scopeAgent = typeof payload.agent === 'string' && payload.agent ? payload.agent : 'subagent' + // Key by the deterministic spanId so two concurrent runs of the SAME agent + // (e.g. two parallel `research` subagents) get distinct trace spans. Fall + // back to agent:parentToolCallId for legacy events that predate span ids. + const traceKey = event.scope?.spanId || `${scopeAgent}:${event.scope?.parentToolCallId || ''}` if (evt === MothershipStreamV1SpanLifecycleEvent.start) { const span = context.trace.startSpan(`subagent:${scopeAgent}`, 'go.subagent', { agent: scopeAgent, parentToolCallId: event.scope?.parentToolCallId, + spanId: event.scope?.spanId, }) context.subAgentTraceSpans ??= new Map() - context.subAgentTraceSpans.set(`${scopeAgent}:${event.scope?.parentToolCallId || ''}`, span) + context.subAgentTraceSpans.set(traceKey, span) } else if (evt === MothershipStreamV1SpanLifecycleEvent.end) { - const key = `${scopeAgent}:${event.scope?.parentToolCallId || ''}` - const span = context.subAgentTraceSpans?.get(key) + const span = context.subAgentTraceSpans?.get(traceKey) if (span) { context.trace.endSpan(span, 'ok') - context.subAgentTraceSpans?.delete(key) + context.subAgentTraceSpans?.delete(traceKey) } } return diff --git a/apps/sim/lib/copilot/request/handlers/text.ts b/apps/sim/lib/copilot/request/handlers/text.ts index 9ad195f4977..16fbc9b6ead 100644 --- a/apps/sim/lib/copilot/request/handlers/text.ts +++ b/apps/sim/lib/copilot/request/handlers/text.ts @@ -24,27 +24,26 @@ export function handleTextEvent(scope: ToolScope): StreamHandler { if (!parentToolCallId) return const spanIdentity = getScopedSpanIdentity(event) if (event.payload.channel === MothershipStreamV1TextChannel.thinking) { - if ( - context.currentSubagentThinkingBlock && - context.currentSubagentThinkingBlock.parentToolCallId !== parentToolCallId - ) { - flushSubagentThinkingBlock(context) - } - if (!context.currentSubagentThinkingBlock) { - context.currentSubagentThinkingBlock = { + // Per-lane thinking: each concurrent subagent accumulates into its own + // block keyed by parentToolCallId, so interleaved chunks from a sibling + // subagent never flush or corrupt this lane's reasoning. + let block = context.subagentThinkingBlocks.get(parentToolCallId) + if (!block) { + block = { type: 'subagent_thinking', content: '', parentToolCallId, ...spanIdentity, timestamp: Date.now(), } + context.subagentThinkingBlocks.set(parentToolCallId, block) } - context.currentSubagentThinkingBlock.content = `${context.currentSubagentThinkingBlock.content || ''}${chunk}` + block.content = `${block.content || ''}${chunk}` return } - if (context.currentSubagentThinkingBlock) { - flushSubagentThinkingBlock(context) - } + // Real text for this lane: close this lane's thinking block first so the + // persisted order is [thinking, text] within the lane. + flushSubagentThinkingBlock(context, parentToolCallId) if (context.isInThinkingBlock) { flushThinkingBlock(context) } diff --git a/apps/sim/lib/copilot/request/handlers/tool.ts b/apps/sim/lib/copilot/request/handlers/tool.ts index 0811917fd1f..7e76a57b2f3 100644 --- a/apps/sim/lib/copilot/request/handlers/tool.ts +++ b/apps/sim/lib/copilot/request/handlers/tool.ts @@ -138,8 +138,14 @@ export async function handleToolEvent( // block into contentBlocks BEFORE we add the tool_call block, or // contentBlocks will end up with tool_call before thinking — which // re-renders on reload in the wrong order (Mothership group above - // the Thinking block, even though thinking happened first). - flushSubagentThinkingBlock(context) + // the Thinking block, even though thinking happened first). A subagent + // tool event flushes only its OWN lane so a concurrent sibling's thinking + // is left intact; a main tool event flushes all subagent lanes. + if (isSubagent && parentToolCallId) { + flushSubagentThinkingBlock(context, parentToolCallId) + } else { + flushSubagentThinkingBlock(context) + } flushThinkingBlock(context) if (isToolResultStreamEvent(event)) { @@ -294,6 +300,11 @@ async function handleCallPhase( const toolCall = context.toolCalls.get(toolCallId) if (!toolCall) return + // Capture the invoking subagent's channel id so the executor can thread it + // into the server tool context — this is what scopes the workspace_file -> + // edit_content intent handoff to one file subagent under concurrency. + if (parentToolCallId) toolCall.parentToolCallId = parentToolCallId + const readPath = typeof args?.path === 'string' ? args.path : undefined if (toolName === 'read' && readPath?.startsWith('internal/')) return diff --git a/apps/sim/lib/copilot/request/handlers/types.ts b/apps/sim/lib/copilot/request/handlers/types.ts index 9f41a996adf..21543ce5d60 100644 --- a/apps/sim/lib/copilot/request/handlers/types.ts +++ b/apps/sim/lib/copilot/request/handlers/types.ts @@ -65,19 +65,45 @@ export function flushThinkingBlock(context: StreamingContext): void { context.currentThinkingBlock = null } -export function flushSubagentThinkingBlock(context: StreamingContext): void { - if (context.currentSubagentThinkingBlock) { - stampBlockEnd(context.currentSubagentThinkingBlock) - context.contentBlocks.push(context.currentSubagentThinkingBlock) +/** + * Flush open subagent thinking blocks into contentBlocks. With a parentToolCallId + * it flushes only that lane (used when a tool/text event arrives for a specific + * subagent); with no argument it flushes ALL open lanes (used at stream end and + * at subagent lifecycle boundaries). Safe to call repeatedly. + */ +export function flushSubagentThinkingBlock( + context: StreamingContext, + parentToolCallId?: string +): void { + if (parentToolCallId !== undefined) { + const block = context.subagentThinkingBlocks.get(parentToolCallId) + if (block) { + stampBlockEnd(block) + context.contentBlocks.push(block) + context.subagentThinkingBlocks.delete(parentToolCallId) + } + return + } + for (const block of context.subagentThinkingBlocks.values()) { + stampBlockEnd(block) + context.contentBlocks.push(block) } - context.currentSubagentThinkingBlock = null + context.subagentThinkingBlocks.clear() } +/** + * Resolve the subagent lane an event belongs to, using ONLY the event's own + * scope. The legacy fallback to a single "current subagent" pointer was removed: + * with concurrent subagents that pointer reflects whichever subagent started + * most recently and would mis-attribute interleaved events. Every subagent-lane + * event is guaranteed to carry parentToolCallId (Go stamps it), so a missing one + * is a real contract violation — callers warn and drop rather than guess. + */ export function getScopedParentToolCallId( event: StreamEvent, - context: StreamingContext + _context: StreamingContext ): string | undefined { - return event.scope?.parentToolCallId || context.subAgentParentToolCallId + return event.scope?.parentToolCallId } /** diff --git a/apps/sim/lib/copilot/request/lifecycle/resume-leg-context.test.ts b/apps/sim/lib/copilot/request/lifecycle/resume-leg-context.test.ts new file mode 100644 index 00000000000..4ffc7bcdbf9 --- /dev/null +++ b/apps/sim/lib/copilot/request/lifecycle/resume-leg-context.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' +import { createStreamingContext } from '@/lib/copilot/request/context/request-context' +import { makeResumeLegContext, mergeResumeLegOutputs } from '@/lib/copilot/request/lifecycle/run' + +// Guards the makeResumeLegContext / mergeResumeLegOutputs contract: the two MUST +// stay in lockstep (every per-leg-isolated scalar is reset on leg creation and +// folded back on merge), and the heavy accumulators stay shared by reference so +// all concurrent legs build one chat. This is the regression the inline comment +// warns about — without per-leg isolation the orchestrator's pre-fanout content +// gets multiplied by the leg count on merge. +describe('resume leg context isolate/merge contract', () => { + it('isolates the per-leg scalars while sharing the heavy accumulators by reference', () => { + const base = createStreamingContext({ + accumulatedContent: 'PRE', + finalAssistantContent: 'PRE-FINAL', + usage: { prompt: 10, completion: 5 }, + cost: { input: 1, output: 2, total: 3 }, + errors: ['pre-existing'], + }) + + const leg = makeResumeLegContext(base) + + // Per-leg scalars reset so a leg accumulates only its OWN output. + expect(leg.accumulatedContent).toBe('') + expect(leg.finalAssistantContent).toBe('') + expect(leg.usage).toBeUndefined() + expect(leg.cost).toBeUndefined() + expect(leg.errors).toEqual([]) + expect(leg.streamComplete).toBe(false) + expect(leg.awaitingAsyncContinuation).toBeUndefined() + + // A leg's own errors array is a fresh array (not the shared one) so a leg's + // retry rollback can't truncate a sibling's errors. + expect(leg.errors).not.toBe(base.errors) + + // Heavy accumulators stay shared by reference (one merged chat). + expect(leg.contentBlocks).toBe(base.contentBlocks) + expect(leg.toolCalls).toBe(base.toolCalls) + expect(leg.pendingToolPromises).toBe(base.pendingToolPromises) + expect(leg.subAgentContent).toBe(base.subAgentContent) + }) + + it('folds a leg back exactly once (no double-count of the orchestrator content)', () => { + const base = createStreamingContext({ accumulatedContent: 'PRE', errors: ['pre'] }) + + const leg = makeResumeLegContext(base) + leg.accumulatedContent = 'JOIN' + leg.finalAssistantContent = 'JOIN-FINAL' + leg.usage = { prompt: 100, completion: 50 } + leg.cost = { input: 4, output: 5, total: 9 } + leg.errors.push('leg-err') + + mergeResumeLegOutputs(base, leg) + + // PRE seeded once + the leg's own output appended once — not PRE+PRE+JOIN. + expect(base.accumulatedContent).toBe('PREJOIN') + expect(base.finalAssistantContent).toBe('JOIN-FINAL') + expect(base.usage).toEqual({ prompt: 100, completion: 50 }) + expect(base.cost).toEqual({ input: 4, output: 5, total: 9 }) + expect(base.errors).toEqual(['pre', 'leg-err']) + }) + + it('does not multiply pre-fanout content across many legs (N children + one join leg)', () => { + const base = createStreamingContext({ accumulatedContent: 'PRE' }) + + // Seven child legs that stream subagent content (not main accumulatedContent) + // contribute nothing to the join scalars; only the join-carrying leg does. + for (let i = 0; i < 7; i++) { + const childLeg = makeResumeLegContext(base) + mergeResumeLegOutputs(base, childLeg) + } + const joinLeg = makeResumeLegContext(base) + joinLeg.accumulatedContent = 'SUMMARY' + joinLeg.usage = { prompt: 1, completion: 1 } + mergeResumeLegOutputs(base, joinLeg) + + // Exactly the pre-fanout content + the one join leg's summary — the 7 child + // legs must not each re-append 'PRE'. + expect(base.accumulatedContent).toBe('PRESUMMARY') + expect(base.usage).toEqual({ prompt: 1, completion: 1 }) + }) +}) diff --git a/apps/sim/lib/copilot/request/lifecycle/run.ts b/apps/sim/lib/copilot/request/lifecycle/run.ts index 01c597c6357..12af8bfe89d 100644 --- a/apps/sim/lib/copilot/request/lifecycle/run.ts +++ b/apps/sim/lib/copilot/request/lifecycle/run.ts @@ -35,6 +35,8 @@ import type { ExecutionContext, OrchestratorOptions, OrchestratorResult, + ResumeContinuation, + ResumeFrame, StreamEvent, StreamingContext, } from '@/lib/copilot/request/types' @@ -231,6 +233,299 @@ export async function runCopilotLifecycle( } } +// --------------------------------------------------------------------------- +// Per-subagent checkpoint resume (concurrent fan-out) +// --------------------------------------------------------------------------- +// +// Under the per-subagent checkpoint model each paused subagent is its OWN +// checkpoint chain (frame.checkpointId) joined at the orchestrator. Instead of +// one bundled /resume, Sim drives one resume chain per child CONCURRENTLY so a +// fast child never waits on a slow sibling, and the Go join wakes the +// orchestrator on whichever child finishes last. Gated by the Go +// `parallel-subagents` flag, surfaced here purely by frames carrying their own +// checkpointId. +// +// IMPORTANT (concurrency): JS is single-threaded, so the legs interleave at await +// points rather than running truly in parallel; shared accumulators +// (contentBlocks, toolCalls maps, errors) are appended via atomic synchronous +// ops and stay shared by reference. Only the per-leg STREAM CONTROL flags +// (streamComplete, awaitingAsyncContinuation) and the join-leg scalars +// (accumulatedContent/usage/cost) are isolated per leg and merged back. + +type AsyncContinuation = ResumeContinuation + +function isPerSubagentContinuation(c: AsyncContinuation): boolean { + return !!c.frames && c.frames.length > 0 && c.frames.every((f) => !!f.checkpointId) +} + +// Shared header set for every Sim -> Go mothership request (initial stream and +// every resume leg), so the auth/source/version headers can't drift between the +// sequential path and the concurrent per-subagent resume legs. +function mothershipRequestHeaders(): Record { + return { + 'Content-Type': 'application/json', + ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), + ...getMothershipSourceEnvHeaders(), + 'X-Client-Version': SIM_AGENT_VERSION, + } +} + +// makeResumeLegContext / mergeResumeLegOutputs are a PAIR and must stay in +// lockstep: every field reset here is folded back there, and nothing else on +// StreamingContext is per-leg. Everything not listed is shared BY REFERENCE +// across all concurrent legs (the one merged chat: contentBlocks, toolCalls, +// pendingToolPromises, subagent maps, etc.). The per-leg ISOLATED set: +// - streamComplete / awaitingAsyncContinuation: stream-control flags, so a +// finished leg can't stop a sibling's read loop (reset only; not merged). +// - accumulatedContent / finalAssistantContent / usage / cost: join-leg +// scalars — only the join-carrying leg sets them; zeroing per leg keeps the +// `+=` merge from multiplying the orchestrator's pre-fanout content by the +// leg count, and keeps a child leg's stale usage/cost from clobbering the +// join leg's real totals on merge. +// - errors: a leg's transient retryable error (rolled back inside +// runResumeLegWithRetry) must not truncate a concurrent sibling's shared +// error array by index; each leg collects its own and merges the survivors. +// When adding a per-leg field, update BOTH functions (and the contract test in +// resume-leg-context.test.ts). Exported only for that test. +export function makeResumeLegContext(base: StreamingContext): StreamingContext { + return { + ...base, + streamComplete: false, + awaitingAsyncContinuation: undefined, + accumulatedContent: '', + finalAssistantContent: '', + usage: undefined, + cost: undefined, + errors: [], + } +} + +// mergeResumeLegOutputs folds a finished leg's isolated scalars back into the +// shared context. Child (subagent-lane) legs leave the join scalars empty; only +// the join-carrying leg (which streams the orchestrator continuation) sets them. +export function mergeResumeLegOutputs(context: StreamingContext, leg: StreamingContext): void { + if (leg.accumulatedContent) context.accumulatedContent += leg.accumulatedContent + if (leg.finalAssistantContent) context.finalAssistantContent += leg.finalAssistantContent + if (leg.usage) context.usage = leg.usage + if (leg.cost) context.cost = leg.cost + if (leg.sawMainToolCall) context.sawMainToolCall = true + if (leg.wasAborted) context.wasAborted = true + if (leg.errors.length > 0) context.errors.push(...leg.errors) +} + +async function waitForToolIds(context: StreamingContext, toolIds: string[]): Promise { + const promises: Promise[] = [] + for (const id of toolIds) { + const p = context.pendingToolPromises.get(id) + if (p) promises.push(p) + } + if (promises.length > 0) await Promise.allSettled(promises) +} + +function collectResultsForToolIds( + context: StreamingContext, + toolIds: string[], + checkpointId: string +): Array<{ callId: string; name: string; data: unknown; success: boolean }> { + return toolIds.map((toolCallId) => { + const tool = context.toolCalls.get(toolCallId) + if (!tool || !tool.result) { + throw new Error( + `Cannot resume subagent chain ${checkpointId}: missing result for tool call ${toolCallId}` + ) + } + return { + callId: toolCallId, + name: tool.name || '', + data: getToolCallTerminalData(tool), + success: requireToolCallStateResult(tool).success, + } + }) +} + +// runResumeLegWithRetry runs ONE resume POST with the same retryable-error + +// bounded-backoff policy the sequential checkpoint loop uses, so a concurrent +// child leg survives a transient Go 5xx (or network blip) instead of failing the +// whole turn — Go releases the claim on such errors expecting a retry. The leg's +// transient error is rolled back on its OWN (isolated) errors array so a +// recovered retry isn't mis-finalized as `error`. An AbortError (a sibling +// failure cancelling this leg, see driveSubagentChains) is non-retryable and +// propagates immediately. +async function runResumeLegWithRetry( + url: string, + body: Record, + leg: StreamingContext, + execContext: ExecutionContext, + options: CopilotLifecycleOptions +): Promise { + let attempt = 0 + for (;;) { + const errorsBeforeAttempt = leg.errors.length + const willRetryOnStreamError = attempt < MAX_RESUME_ATTEMPTS - 1 + const legBody = willRetryOnStreamError ? { ...body, willRetryOnStreamError: true } : body + try { + await runStreamLoop( + url, + { method: 'POST', headers: mothershipRequestHeaders(), body: JSON.stringify(legBody) }, + leg, + execContext, + options + ) + return + } catch (error) { + if (isRetryableStreamError(error) && attempt < MAX_RESUME_ATTEMPTS - 1) { + leg.errors.length = errorsBeforeAttempt + attempt++ + const backoff = RESUME_BACKOFF_MS[attempt - 1] ?? 1000 + logger.warn('Child resume leg failed, retrying', { + attempt: attempt + 1, + maxAttempts: MAX_RESUME_ATTEMPTS, + backoffMs: backoff, + error: toError(error).message, + }) + await sleepWithAbort(backoff, options.abortSignal) + continue + } + throw error + } + } +} + +// driveOneChildChain resumes a single subagent's checkpoint chain to its end: +// resume -> (re-pause -> resume)* -> fold into join. Returns the orchestrator's +// follow-on continuation when THIS leg is the one the Go join woke (the last +// finisher whose /resume response carried the orchestrator continuation), else +// null. Re-pause vs follow-on is disambiguated by checkpoint id: a re-pause keeps +// the same child id; the join continuation is a different (orchestrator) id. +async function driveOneChildChain( + frame: ResumeFrame, + context: StreamingContext, + execContext: ExecutionContext, + options: CopilotLifecycleOptions, + baseURL: string, + workspaceId?: string +): Promise { + // ParentToolCallID is the SAME subagent's stable identity across re-pauses; + // the checkpoint id rotates each re-pause (the prior one is already claimed). + const parentToolCallId = frame.parentToolCallId + // Guarded (not cast): a per-subagent frame always carries its own checkpointId + // (isPerSubagentContinuation requires it), but a local guard keeps this driver + // correct on its own terms rather than trusting a caller-side invariant. + if (!frame.checkpointId) return null + let checkpointId = frame.checkpointId + let toolIds = frame.pendingToolIds + + for (;;) { + if (isAborted(options, context)) return null + + await waitForToolIds(context, toolIds) + const results = collectResultsForToolIds(context, toolIds, checkpointId) + + const leg = makeResumeLegContext(context) + await runResumeLegWithRetry( + `${baseURL}/api/tools/resume`, + { + streamId: context.messageId, + checkpointId, + userId: options.userId, + ...(workspaceId ? { workspaceId } : {}), + results, + }, + leg, + execContext, + options + ) + mergeResumeLegOutputs(context, leg) + + const cont = leg.awaitingAsyncContinuation + if (!cont) { + // The last finisher's leg, whose join continuation streamed the + // orchestrator to completion (done): nothing more to drive on this leg. + return null + } + // A NON-last finisher folds with a TERMINAL pause carrying the join id but + // NO pending tools and NO frames — the child's work is done and the join + // wakes on whichever sibling finishes last. End this leg cleanly; do NOT + // mistake the join id for an orchestrator follow-on and try to resume it. + const hasPending = (cont.pendingToolCallIds?.length ?? 0) > 0 + const hasFrames = (cont.frames?.length ?? 0) > 0 + if (!hasPending && !hasFrames) { + return null + } + // Re-pause is identified by THIS subagent's stable parentToolCallId (the + // checkpoint id rotates each re-pause). If present, keep driving this child + // with its new id + leaves. + const repaused = cont.frames?.find( + (f) => f.parentToolCallId === parentToolCallId && f.checkpointId + ) + if (repaused?.checkpointId) { + checkpointId = repaused.checkpointId + toolIds = repaused.pendingToolIds + continue + } + // No frame for this subagent => the join fired and the orchestrator re-paused + // on this leg. Hand it back to the main loop to continue the turn. + return cont + } +} + +// driveSubagentChains fans out one resume chain per child frame concurrently and +// returns the single orchestrator follow-on continuation (if the orchestrator +// re-paused after the join), or null when the turn completed. +// +// Failure isolation: the legs share a per-fanout AbortController so the FIRST leg +// to fail cancels its siblings' in-flight resumes (otherwise a `Promise.all` +// reject leaves the siblings running detached — still mutating shared context and +// POSTing /resume after the turn has errored). The controller also chains off the +// caller's abort signal so a user stop cancels every leg. Each leg's failure is +// caught (so Promise.all can't reject before its siblings unwind); we then +// rethrow the first REAL error, not the AbortErrors it triggered in the siblings. +async function driveSubagentChains( + continuation: AsyncContinuation, + context: StreamingContext, + execContext: ExecutionContext, + options: CopilotLifecycleOptions, + baseURL: string, + workspaceId?: string +): Promise { + const frames = continuation.frames ?? [] + logger.info('Driving subagent checkpoint chains concurrently', { + childCount: frames.length, + checkpointIds: frames.map((f) => f.checkpointId), + }) + + const fanoutController = new AbortController() + const parentSignal = options.abortSignal + const onParentAbort = () => fanoutController.abort() + if (parentSignal) { + if (parentSignal.aborted) fanoutController.abort() + else parentSignal.addEventListener('abort', onParentAbort, { once: true }) + } + const legOptions: CopilotLifecycleOptions = { ...options, abortSignal: fanoutController.signal } + + let firstError: unknown + try { + const followOns = await Promise.all( + frames.map((frame) => + driveOneChildChain(frame, context, execContext, legOptions, baseURL, workspaceId).catch( + (error) => { + // First real failure wins and cancels the siblings; their resulting + // AbortErrors arrive later and don't overwrite it. Swallow here so + // Promise.all doesn't reject before every leg has unwound. + if (firstError === undefined) firstError = error + fanoutController.abort() + return null + } + ) + ) + ) + if (firstError !== undefined) throw firstError + return followOns.find((c): c is AsyncContinuation => !!c) ?? null + } finally { + parentSignal?.removeEventListener('abort', onParentAbort) + } +} + // --------------------------------------------------------------------------- // Checkpoint loop – the core state machine // --------------------------------------------------------------------------- @@ -339,12 +634,7 @@ async function runCheckpointLoop( `${mothershipBaseURL}${route}`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), - ...getMothershipSourceEnvHeaders(), - 'X-Client-Version': SIM_AGENT_VERSION, - }, + headers: mothershipRequestHeaders(), body: JSON.stringify(legPayload), }, context, @@ -405,9 +695,38 @@ async function runCheckpointLoop( break } - const continuation = context.awaitingAsyncContinuation + let continuation = context.awaitingAsyncContinuation if (!continuation) break + // Per-subagent checkpoint model: fan out one concurrent resume chain per + // child instead of a single bundled resume. The driver returns null when the + // turn completed, or the orchestrator's follow-on continuation when it + // re-paused after the join. A per-subagent follow-on (orchestrator spawned + // more subagents) loops back through the driver; a normal follow-on falls + // through to the sequential resume path below. + if (isPerSubagentContinuation(continuation)) { + context.awaitingAsyncContinuation = undefined + let next: AsyncContinuation | null = continuation + while (next && isPerSubagentContinuation(next)) { + if (isAborted(options, context)) { + cancelPendingTools(context) + next = null + break + } + await waitForToolIds(context, next.pendingToolCallIds) + next = await driveSubagentChains( + next, + context, + execContext, + options, + mothershipBaseURL, + lifecycleWorkspaceId + ) + } + if (!next) break + continuation = next + } + if (context.pendingToolPromises.size > 0) { // Bounded by the slowest pending tool's watchdog plus grace. The // per-tool watchdog already guarantees each promise settles; this gate diff --git a/apps/sim/lib/copilot/request/session/buffer.test.ts b/apps/sim/lib/copilot/request/session/buffer.test.ts index 6aa9bc0752a..1556ed7c3d5 100644 --- a/apps/sim/lib/copilot/request/session/buffer.test.ts +++ b/apps/sim/lib/copilot/request/session/buffer.test.ts @@ -166,7 +166,7 @@ describe('mothership-stream-outbox', () => { expect(mockRedis.zremrangebyrank).toHaveBeenCalledWith( 'mothership_stream:stream-1:events', 0, - -5_001 + -100_001 ) }) diff --git a/apps/sim/lib/copilot/request/session/buffer.ts b/apps/sim/lib/copilot/request/session/buffer.ts index 810b0cd5a88..871bdeb8d25 100644 --- a/apps/sim/lib/copilot/request/session/buffer.ts +++ b/apps/sim/lib/copilot/request/session/buffer.ts @@ -13,7 +13,7 @@ const logger = createLogger('SessionBuffer') const STREAM_OUTBOX_PREFIX = 'mothership_stream:' const DEFAULT_TTL_SECONDS = 60 * 60 const DEFAULT_COMPLETED_TTL_SECONDS = 5 * 60 -const DEFAULT_EVENT_LIMIT = 5_000 +const DEFAULT_EVENT_LIMIT = 100_000 const RETRY_DELAYS_MS = [0, 50, 150] as const type RedisOperationMetadata = { diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index b1637b017ce..f479d5e99b7 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -265,7 +265,13 @@ class ToolExecutionTimeoutError extends Error { */ async function executeToolWithWatchdog(toolCall: ToolCallState, execContext: ExecutionContext) { const timeoutMs = toolWatchdogTimeoutMs(toolCall.name) - const execution = executeTool(toolCall.name, toolCall.params || {}, execContext) + // Thread the invoking subagent's channel id per call (execContext is shared + // across the whole turn, so the channel id can't live on it) — server tools + // use it to scope the workspace_file -> edit_content intent handoff. + const toolContext = toolCall.parentToolCallId + ? { ...execContext, parentToolCallId: toolCall.parentToolCallId } + : execContext + const execution = executeTool(toolCall.name, toolCall.params || {}, toolContext) let timer: ReturnType | undefined try { return await Promise.race([ diff --git a/apps/sim/lib/copilot/request/types.ts b/apps/sim/lib/copilot/request/types.ts index 0898bdbe442..2efa49fdbcc 100644 --- a/apps/sim/lib/copilot/request/types.ts +++ b/apps/sim/lib/copilot/request/types.ts @@ -28,6 +28,14 @@ export interface ToolCallState { error?: string startTime?: number endTime?: number + /** + * For a subagent-scoped tool call, the invoking subagent's channel id (its + * outer tool_use id, = event.scope.parentToolCallId). Captured at dispatch so + * the executor can thread it into the server tool context and scope the + * workspace_file -> edit_content intent handoff per file subagent. Undefined + * for main-lane tool calls. + */ + parentToolCallId?: string } export type ToolCallResult = ToolExecutionResult & { @@ -67,6 +75,41 @@ export interface ContentBlock { parentSpanId?: string } +export interface ActiveFileIntent { + toolCallId: string + operation: string + target: { kind: string; fileId?: string; fileName?: string; path?: string } + title?: string + contentType?: string + edit?: Record +} + +// One paused subagent frame in an async continuation. Mirrors the wire +// MothershipStreamV1CheckpointPauseFrame the run handler maps from, but is the +// internal shape the resume driver consumes (named once here so the lifecycle +// driver and handlers reference the same type instead of re-declaring it inline). +export interface ResumeFrame { + parentToolCallId: string + parentToolName: string + pendingToolIds: string[] + // Per-subagent checkpoint model: this frame's OWN checkpoint chain. When set, + // the resume loop must POST /api/tools/resume with THIS id (not the top-level + // checkpointId) carrying only this frame's leaf results, and may drive the N + // frames concurrently. Empty under the bundled-frame model. + checkpointId?: string +} + +// The async-continuation state captured from a checkpoint_pause: what the resume +// loop needs to drive the next /resume (the bundled top-level id + pending tools, +// or per-subagent frames each carrying their own checkpointId). +export interface ResumeContinuation { + checkpointId: string + executionId?: string + runId?: string + pendingToolCallIds: string[] + frames?: ResumeFrame[] +} + export interface StreamingContext { chatId?: string requestId?: string @@ -79,22 +122,16 @@ export interface StreamingContext { contentBlocks: ContentBlock[] toolCalls: Map pendingToolPromises: Map> - awaitingAsyncContinuation?: { - checkpointId: string - executionId?: string - runId?: string - pendingToolCallIds: string[] - frames?: Array<{ - parentToolCallId: string - parentToolName: string - pendingToolIds: string[] - }> - } + awaitingAsyncContinuation?: ResumeContinuation currentThinkingBlock: ContentBlock | null - currentSubagentThinkingBlock: ContentBlock | null + /** + * Open subagent "thinking" blocks, keyed by parentToolCallId (one lane per + * concurrent subagent). Was a single slot, which collided when two subagents + * streamed thinking concurrently — interleaved chunks flushed each other's + * block. Per-lane keying keeps each subagent's reasoning intact. + */ + subagentThinkingBlocks: Map isInThinkingBlock: boolean - subAgentParentToolCallId?: string - subAgentParentStack: string[] subAgentContent: Record subAgentToolCalls: Record openSubagentParents?: Set @@ -104,14 +141,14 @@ export interface StreamingContext { errors: string[] usage?: { prompt: number; completion: number } cost?: { input: number; output: number; total: number } - activeFileIntent?: { - toolCallId: string - operation: string - target: { kind: string; fileId?: string; fileName?: string; path?: string } - title?: string - contentType?: string - edit?: Record - } | null + /** + * In-flight file-write intents keyed by the file subagent's channel id + * (event.scope.parentToolCallId). Was a single slot, which cross-attributed + * streamed content when two file subagents wrote concurrently; per-channel + * keying isolates each agent's preview. The empty-string key holds the + * main-lane / no-scope intent (file writes there are always sequential). + */ + activeFileIntents: Map trace: TraceCollector subAgentTraceSpans?: Map } diff --git a/apps/sim/lib/copilot/tool-executor/types.ts b/apps/sim/lib/copilot/tool-executor/types.ts index 9087f38f634..f1c2eae27f5 100644 --- a/apps/sim/lib/copilot/tool-executor/types.ts +++ b/apps/sim/lib/copilot/tool-executor/types.ts @@ -11,6 +11,13 @@ export interface ToolExecutionContext { copilotToolExecution?: boolean requestMode?: string currentAgentId?: string + /** + * The invoking subagent's channel id (its outer tool_use id), threaded per + * tool call so server tools can scope state to one subagent invocation. Two + * concurrent file subagents share currentAgentId ("file") but have distinct + * parentToolCallIds, so this — not currentAgentId — disambiguates them. + */ + parentToolCallId?: string abortSignal?: AbortSignal userTimezone?: string userPermission?: string diff --git a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts index 777c4649019..a83fa4a8b84 100644 --- a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts +++ b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts @@ -20,6 +20,7 @@ export function createServerToolHandler(toolId: string): ToolHandler { userPermission: context.userPermission ?? undefined, chatId: context.chatId, messageId: context.messageId, + parentToolCallId: context.parentToolCallId, abortSignal: context.abortSignal, }) diff --git a/apps/sim/lib/copilot/tools/server/base-tool.ts b/apps/sim/lib/copilot/tools/server/base-tool.ts index 6131d552962..dc03ae805b2 100644 --- a/apps/sim/lib/copilot/tools/server/base-tool.ts +++ b/apps/sim/lib/copilot/tools/server/base-tool.ts @@ -6,6 +6,13 @@ export interface ServerToolContext { userPermission?: string chatId?: string messageId?: string + /** + * The invoking subagent's channel id (its outer tool_use id). Used to scope + * the workspace_file -> edit_content intent handoff to a single file subagent + * so two file agents writing concurrently never consume each other's pending + * intent. Undefined for main-agent tool calls (which never overlap). + */ + parentToolCallId?: string abortSignal?: AbortSignal /** Fires only on explicit user stop, never on passive transport disconnect. */ userStopSignal?: AbortSignal diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.ts index 8bedce47b41..a84cea989d4 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.ts @@ -49,9 +49,15 @@ export const editContentServerTool: BaseServerTool ({ getRedisClient: () => null })) + +function makeIntent(overrides: Partial): PendingFileIntent { + return { + operation: 'update', + fileId: 'file-x', + workspaceId: 'ws-1', + userId: 'user-1', + chatId: 'chat-1', + messageId: 'msg-1', + fileRecord: { id: overrides.fileId ?? 'file-x' } as unknown as PendingFileIntent['fileRecord'], + createdAt: Date.now(), + ...overrides, + } +} + +function uniqueWorkspace(): string { + return `ws-${Math.random().toString(36).slice(2)}` +} + +describe('file-intent-store channel scoping', () => { + it('consumes the intent for the requesting channel, not the latest in the message', async () => { + const ws = uniqueWorkspace() + const scope = { chatId: 'chat-1', messageId: 'msg-1' } + + // Two concurrent file subagents: A declares fileA on channel F1 first, then + // B declares fileB on channel F2 (later createdAt = the "latest" in message). + await storeFileIntent( + ws, + 'fileA', + makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() }) + ) + await storeFileIntent( + ws, + 'fileB', + makeIntent({ + workspaceId: ws, + fileId: 'fileB', + channelId: 'F2', + createdAt: Date.now() + 1000, + }) + ) + + // edit_content from channel F1 must get fileA — NOT the latest (fileB). + const a = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F1' }) + expect(a?.fileId).toBe('fileA') + + // edit_content from channel F2 gets fileB. + const b = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F2' }) + expect(b?.fileId).toBe('fileB') + }) + + it('only consumes its own channel, leaving the sibling intent intact', async () => { + const ws = uniqueWorkspace() + const scope = { chatId: 'chat-1', messageId: 'msg-1' } + await storeFileIntent( + ws, + 'fileA', + makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() }) + ) + await storeFileIntent( + ws, + 'fileB', + makeIntent({ + workspaceId: ws, + fileId: 'fileB', + channelId: 'F2', + createdAt: Date.now() + 1000, + }) + ) + + await consumeLatestFileIntent(ws, { ...scope, channelId: 'F1' }) + // The sibling (F2) is untouched and still consumable afterward. + const b = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F2' }) + expect(b?.fileId).toBe('fileB') + }) + + it('falls back to latest-in-message when no channelId (legacy / main-agent)', async () => { + const ws = uniqueWorkspace() + const scope = { chatId: 'chat-1', messageId: 'msg-1' } + await storeFileIntent( + ws, + 'fileA', + makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() }) + ) + await storeFileIntent( + ws, + 'fileB', + makeIntent({ + workspaceId: ws, + fileId: 'fileB', + channelId: 'F2', + createdAt: Date.now() + 1000, + }) + ) + const latest = await consumeLatestFileIntent(ws, scope) + expect(latest?.fileId).toBe('fileB') + }) + + it('returns undefined when the requesting channel has no pending intent', async () => { + const ws = uniqueWorkspace() + await storeFileIntent( + ws, + 'fileA', + makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() }) + ) + const none = await consumeLatestFileIntent(ws, { + chatId: 'chat-1', + messageId: 'msg-1', + channelId: 'F-absent', + }) + expect(none).toBeUndefined() + }) +}) diff --git a/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts b/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts index a7a0a51fff9..82f7977b17d 100644 --- a/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts +++ b/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts @@ -11,6 +11,11 @@ export type PendingFileIntent = { userId: string chatId?: string messageId?: string + // The invoking file subagent's channel id (its outer tool_use id). Lets + // edit_content consume the intent for ITS OWN file subagent instead of the + // latest in the message, so two file agents writing concurrently never cross + // their content into each other's file. + channelId?: string fileRecord: WorkspaceFileRecord existingContent?: string edit?: { @@ -33,6 +38,10 @@ export type PendingFileIntent = { export type FileIntentScope = { chatId?: string messageId?: string + // When set, consumeLatestFileIntent only considers intents from this subagent + // channel — the key to isolating concurrent file subagents. Omitted by callers + // that intentionally span the whole message (e.g. clearIntentsForWorkspace). + channelId?: string } const logger = createLogger('FileIntentStore') @@ -55,6 +64,14 @@ function scopeMatches(intent: PendingFileIntent, scope?: FileIntentScope): boole return intent.chatId === scope?.chatId && intent.messageId === scope?.messageId } +// Channel filter for consume: when a scope carries a channelId, only the +// matching file subagent's intent qualifies. No channelId => message-wide +// (legacy / main-agent) behavior. Deliberately separate from scopeMatches so +// clearIntentsForWorkspace keeps clearing every channel in a message. +function channelMatches(intent: PendingFileIntent, scope?: FileIntentScope): boolean { + return !scope?.channelId || intent.channelId === scope.channelId +} + function buildScopedField(fileId: string, scope?: FileIntentScope): string { return `${scope?.chatId ?? ''}:${scope?.messageId ?? ''}:${fileId}` } @@ -200,7 +217,11 @@ export async function consumeLatestFileIntent( let latest: PendingFileIntent | undefined let latestKey: string | undefined for (const [key, intent] of memoryStore) { - if (intent.workspaceId === workspaceId && scopeMatches(intent, scope)) { + if ( + intent.workspaceId === workspaceId && + scopeMatches(intent, scope) && + channelMatches(intent, scope) + ) { if (!latest || intent.createdAt > latest.createdAt) { latest = intent latestKey = key @@ -225,7 +246,7 @@ export async function consumeLatestFileIntent( staleFields.push(field) continue } - if (!scopeMatches(parsed, scope)) { + if (!scopeMatches(parsed, scope) || !channelMatches(parsed, scope)) { continue } if (!latest || parsed.createdAt > latest.createdAt) { diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index bcea17ddb7d..a490441d4cc 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -428,6 +428,7 @@ export const workspaceFileServerTool: BaseServerTool = { sixtyfour: SixtyfourIcon, slack: SlackIcon, smtp: SmtpIcon, + sportmonks: SportmonksIcon, sqs: SQSIcon, square: SquareIcon, ssh: SshIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 45ecb7990fd..ad85368dcfa 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-17", + "updatedAt": "2026-06-18", "integrations": [ { "type": "onepassword", @@ -14931,6 +14931,217 @@ "category": "tools", "integrationType": "email" }, + { + "type": "sportmonks", + "slug": "sportmonks", + "name": "Sportmonks", + "description": "Access Sportmonks football, motorsport, odds, and reference data", + "longDescription": "Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, teams, squads, players, standings, and topscorers. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones.", + "bgColor": "#171534", + "iconName": "SportmonksIcon", + "docsUrl": "https://docs.sim.ai/integrations/sportmonks", + "operations": [ + { + "name": "Get Live Football Scores", + "description": "Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks" + }, + { + "name": "Get Inplay Football Scores", + "description": "Retrieve all fixtures that are currently being played (in-play) from Sportmonks" + }, + { + "name": "Get Football Fixtures by Date", + "description": "Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks" + }, + { + "name": "Get Football Fixtures by Date Range", + "description": "Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max range is 100 days." + }, + { + "name": "Get Football Fixture by ID", + "description": "Retrieve a single football fixture by its ID from Sportmonks" + }, + { + "name": "Get Football Head to Head", + "description": "Retrieve the head-to-head fixtures between two teams from Sportmonks" + }, + { + "name": "Get Football Leagues", + "description": "Retrieve all football leagues available within your Sportmonks subscription" + }, + { + "name": "Get Football League by ID", + "description": "Retrieve a single football league by its ID from Sportmonks" + }, + { + "name": "Search Football Teams", + "description": "Search for football teams by name from Sportmonks" + }, + { + "name": "Get Football Team by ID", + "description": "Retrieve a single football team by its ID from Sportmonks" + }, + { + "name": "Get Football Team Squad", + "description": "Retrieve the current domestic squad for a team by team ID from Sportmonks" + }, + { + "name": "Search Football Players", + "description": "Search for football players by name from Sportmonks" + }, + { + "name": "Get Football Player by ID", + "description": "Retrieve a single football player by their ID from Sportmonks" + }, + { + "name": "Get Football Standings by Season", + "description": "Retrieve the full league standings table for a season by season ID from Sportmonks" + }, + { + "name": "Get Football Topscorers by Season", + "description": "Retrieve the topscorers (goals, assists, cards) for a season by season ID from Sportmonks" + }, + { + "name": "Get Live Motorsport Scores", + "description": "Retrieve all live motorsport fixtures (sessions) from Sportmonks" + }, + { + "name": "Get Motorsport Fixtures by Date", + "description": "Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks" + }, + { + "name": "Get Motorsport Fixture by ID", + "description": "Retrieve a single motorsport fixture (session) by its ID from Sportmonks" + }, + { + "name": "Get Motorsport Drivers", + "description": "Retrieve all motorsport drivers from Sportmonks" + }, + { + "name": "Get Motorsport Driver by ID", + "description": "Retrieve a single motorsport driver by their ID from Sportmonks" + }, + { + "name": "Search Motorsport Drivers", + "description": "Search for motorsport drivers by name from Sportmonks" + }, + { + "name": "Get Motorsport Teams", + "description": "Retrieve all motorsport teams (constructors) from Sportmonks" + }, + { + "name": "Get Motorsport Team by ID", + "description": "Retrieve a single motorsport team (constructor) by its ID from Sportmonks" + }, + { + "name": "Get Motorsport Driver Standings by Season", + "description": "Retrieve the drivers championship standings for a season by season ID from Sportmonks" + }, + { + "name": "Get Motorsport Team Standings by Season", + "description": "Retrieve the constructors championship standings for a season by season ID from Sportmonks" + }, + { + "name": "Get Motorsport Laps by Fixture", + "description": "Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmonks" + }, + { + "name": "Get Motorsport Pitstops by Fixture", + "description": "Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Sportmonks" + }, + { + "name": "Get Pre-match Odds by Fixture", + "description": "Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API" + }, + { + "name": "Get In-play Odds by Fixture", + "description": "Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API" + }, + { + "name": "Get Bookmakers", + "description": "Retrieve all bookmakers from the Sportmonks Odds API" + }, + { + "name": "Get Bookmaker by ID", + "description": "Retrieve a single bookmaker by its ID from the Sportmonks Odds API" + }, + { + "name": "Search Bookmakers", + "description": "Search for bookmakers by name from the Sportmonks Odds API" + }, + { + "name": "Get Betting Markets", + "description": "Retrieve all betting markets from the Sportmonks Odds API" + }, + { + "name": "Get Betting Market by ID", + "description": "Retrieve a single betting market by its ID from the Sportmonks Odds API" + }, + { + "name": "Search Betting Markets", + "description": "Search for betting markets by name from the Sportmonks Odds API" + }, + { + "name": "Get Continents", + "description": "Retrieve all continents from the Sportmonks Core API" + }, + { + "name": "Get Continent by ID", + "description": "Retrieve a single continent by its ID from the Sportmonks Core API" + }, + { + "name": "Get Countries", + "description": "Retrieve all countries from the Sportmonks Core API" + }, + { + "name": "Get Country by ID", + "description": "Retrieve a single country by its ID from the Sportmonks Core API" + }, + { + "name": "Search Countries", + "description": "Search for countries by name from the Sportmonks Core API" + }, + { + "name": "Get Regions", + "description": "Retrieve all regions from the Sportmonks Core API" + }, + { + "name": "Get Region by ID", + "description": "Retrieve a single region by its ID from the Sportmonks Core API" + }, + { + "name": "Get Cities", + "description": "Retrieve all cities from the Sportmonks Core API" + }, + { + "name": "Get City by ID", + "description": "Retrieve a single city by its ID from the Sportmonks Core API" + }, + { + "name": "Search Cities", + "description": "Search for cities by name from the Sportmonks Core API" + }, + { + "name": "Get Types", + "description": "Retrieve all types (reference data describing events, statistics, positions, etc.) from the Sportmonks Core API" + }, + { + "name": "Get Type by ID", + "description": "Retrieve a single type by its ID from the Sportmonks Core API" + }, + { + "name": "Get Timezones", + "description": "Retrieve all supported time zones (IANA names) from the Sportmonks Core API" + } + ], + "operationCount": 48, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "analytics", + "tags": ["data-analytics"] + }, { "type": "square", "slug": "square", @@ -15413,7 +15624,7 @@ "slug": "supabase", "name": "Supabase", "description": "Use Supabase database", - "longDescription": "Integrate Supabase into the workflow. Supports database operations (query, insert, update, delete, upsert), full-text search, RPC functions, row counting, vector search, and complete storage management (upload, download, list, move, copy, delete files and buckets).", + "longDescription": "Integrate Supabase into the workflow. Supports database operations (query, insert, update, delete, upsert), full-text search, RPC functions, Edge Function invocation, row counting, vector search, and complete storage management (upload, download, list, move, copy, delete files and buckets).", "bgColor": "#1C1C1C", "iconName": "SupabaseIcon", "docsUrl": "https://docs.sim.ai/integrations/supabase", @@ -15458,6 +15669,10 @@ "name": "Call RPC Function", "description": "Call a PostgreSQL function in Supabase" }, + { + "name": "Invoke Edge Function", + "description": "Invoke a Supabase Edge Function over HTTP" + }, { "name": "Introspect Schema", "description": "Introspect Supabase database schema to get table structures, columns, and relationships" @@ -15507,7 +15722,7 @@ "description": "Delete a storage bucket in Supabase" } ], - "operationCount": 22, + "operationCount": 23, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 18ec21ad39b..43d9eb05cb3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3102,6 +3102,62 @@ import { } from '@/tools/slack' import { smsSendTool } from '@/tools/sms' import { smtpSendMailTool } from '@/tools/smtp' +import { + sportmonksCoreGetCitiesTool, + sportmonksCoreGetCityTool, + sportmonksCoreGetContinentsTool, + sportmonksCoreGetContinentTool, + sportmonksCoreGetCountriesTool, + sportmonksCoreGetCountryTool, + sportmonksCoreGetRegionsTool, + sportmonksCoreGetRegionTool, + sportmonksCoreGetTimezonesTool, + sportmonksCoreGetTypesTool, + sportmonksCoreGetTypeTool, + sportmonksCoreSearchCitiesTool, + sportmonksCoreSearchCountriesTool, +} from '@/tools/sportmonks_core' +import { + sportmonksGetFixturesByDateRangeTool, + sportmonksGetFixturesByDateTool, + sportmonksGetFixtureTool, + sportmonksGetHeadToHeadTool, + sportmonksGetInplayLivescoresTool, + sportmonksGetLeaguesTool, + sportmonksGetLeagueTool, + sportmonksGetLivescoresTool, + sportmonksGetPlayerTool, + sportmonksGetStandingsBySeasonTool, + sportmonksGetTeamSquadTool, + sportmonksGetTeamTool, + sportmonksGetTopscorersBySeasonTool, + sportmonksSearchPlayersTool, + sportmonksSearchTeamsTool, +} from '@/tools/sportmonks_football' +import { + sportmonksMotorsportGetDriverStandingsBySeasonTool, + sportmonksMotorsportGetDriversTool, + sportmonksMotorsportGetDriverTool, + sportmonksMotorsportGetFixturesByDateTool, + sportmonksMotorsportGetFixtureTool, + sportmonksMotorsportGetLapsByFixtureTool, + sportmonksMotorsportGetLivescoresTool, + sportmonksMotorsportGetPitstopsByFixtureTool, + sportmonksMotorsportGetTeamStandingsBySeasonTool, + sportmonksMotorsportGetTeamsTool, + sportmonksMotorsportGetTeamTool, + sportmonksMotorsportSearchDriversTool, +} from '@/tools/sportmonks_motorsport' +import { + sportmonksOddsGetBookmakersTool, + sportmonksOddsGetBookmakerTool, + sportmonksOddsGetInplayOddsByFixtureTool, + sportmonksOddsGetMarketsTool, + sportmonksOddsGetMarketTool, + sportmonksOddsGetPreMatchOddsByFixtureTool, + sportmonksOddsSearchBookmakersTool, + sportmonksOddsSearchMarketsTool, +} from '@/tools/sportmonks_odds' import { spotifyAddPlaylistCoverTool, spotifyAddToQueueTool, @@ -4199,6 +4255,56 @@ export const tools: Record = { sendgrid_delete_template: sendGridDeleteTemplateTool, sendgrid_create_template_version: sendGridCreateTemplateVersionTool, smtp_send_mail: smtpSendMailTool, + sportmonks_football_get_fixtures_by_date: sportmonksGetFixturesByDateTool, + sportmonks_football_get_fixtures_by_date_range: sportmonksGetFixturesByDateRangeTool, + sportmonks_football_get_fixture: sportmonksGetFixtureTool, + sportmonks_football_get_head_to_head: sportmonksGetHeadToHeadTool, + sportmonks_football_get_livescores: sportmonksGetLivescoresTool, + sportmonks_football_get_inplay_livescores: sportmonksGetInplayLivescoresTool, + sportmonks_football_get_leagues: sportmonksGetLeaguesTool, + sportmonks_football_get_league: sportmonksGetLeagueTool, + sportmonks_football_search_teams: sportmonksSearchTeamsTool, + sportmonks_football_get_team: sportmonksGetTeamTool, + sportmonks_football_get_team_squad: sportmonksGetTeamSquadTool, + sportmonks_football_search_players: sportmonksSearchPlayersTool, + sportmonks_football_get_player: sportmonksGetPlayerTool, + sportmonks_football_get_standings_by_season: sportmonksGetStandingsBySeasonTool, + sportmonks_football_get_topscorers_by_season: sportmonksGetTopscorersBySeasonTool, + sportmonks_core_get_continents: sportmonksCoreGetContinentsTool, + sportmonks_core_get_continent: sportmonksCoreGetContinentTool, + sportmonks_core_get_countries: sportmonksCoreGetCountriesTool, + sportmonks_core_get_country: sportmonksCoreGetCountryTool, + sportmonks_core_search_countries: sportmonksCoreSearchCountriesTool, + sportmonks_core_get_regions: sportmonksCoreGetRegionsTool, + sportmonks_core_get_region: sportmonksCoreGetRegionTool, + sportmonks_core_get_cities: sportmonksCoreGetCitiesTool, + sportmonks_core_get_city: sportmonksCoreGetCityTool, + sportmonks_core_search_cities: sportmonksCoreSearchCitiesTool, + sportmonks_core_get_types: sportmonksCoreGetTypesTool, + sportmonks_core_get_type: sportmonksCoreGetTypeTool, + sportmonks_core_get_timezones: sportmonksCoreGetTimezonesTool, + sportmonks_motorsport_get_livescores: sportmonksMotorsportGetLivescoresTool, + sportmonks_motorsport_get_fixtures_by_date: sportmonksMotorsportGetFixturesByDateTool, + sportmonks_motorsport_get_fixture: sportmonksMotorsportGetFixtureTool, + sportmonks_motorsport_get_drivers: sportmonksMotorsportGetDriversTool, + sportmonks_motorsport_get_driver: sportmonksMotorsportGetDriverTool, + sportmonks_motorsport_search_drivers: sportmonksMotorsportSearchDriversTool, + sportmonks_motorsport_get_teams: sportmonksMotorsportGetTeamsTool, + sportmonks_motorsport_get_team: sportmonksMotorsportGetTeamTool, + sportmonks_motorsport_get_driver_standings_by_season: + sportmonksMotorsportGetDriverStandingsBySeasonTool, + sportmonks_motorsport_get_team_standings_by_season: + sportmonksMotorsportGetTeamStandingsBySeasonTool, + sportmonks_motorsport_get_laps_by_fixture: sportmonksMotorsportGetLapsByFixtureTool, + sportmonks_motorsport_get_pitstops_by_fixture: sportmonksMotorsportGetPitstopsByFixtureTool, + sportmonks_odds_get_pre_match_odds_by_fixture: sportmonksOddsGetPreMatchOddsByFixtureTool, + sportmonks_odds_get_inplay_odds_by_fixture: sportmonksOddsGetInplayOddsByFixtureTool, + sportmonks_odds_get_bookmakers: sportmonksOddsGetBookmakersTool, + sportmonks_odds_get_bookmaker: sportmonksOddsGetBookmakerTool, + sportmonks_odds_search_bookmakers: sportmonksOddsSearchBookmakersTool, + sportmonks_odds_get_markets: sportmonksOddsGetMarketsTool, + sportmonks_odds_get_market: sportmonksOddsGetMarketTool, + sportmonks_odds_search_markets: sportmonksOddsSearchMarketsTool, sftp_upload: sftpUploadTool, sftp_download: sftpDownloadTool, sftp_list: sftpListTool, diff --git a/apps/sim/tools/sportmonks/types.ts b/apps/sim/tools/sportmonks/types.ts new file mode 100644 index 00000000000..00dc3aa181c --- /dev/null +++ b/apps/sim/tools/sportmonks/types.ts @@ -0,0 +1,89 @@ +import type { OutputProperty } from '@/tools/types' + +/** + * Shared helpers and types for all Sportmonks APIs (football, motorsport, odds, + * core). This module is intentionally vendor-generic — it carries no + * sport-specific base URL or entity shapes. Each Sportmonks integration lives in + * its own `sportmonks_{api}` directory and imports these helpers from here. + */ + +/** + * Parameters shared by every Sportmonks tool. The API token is sent via the + * `Authorization` header, while `include`/`filters` are appended to the query. + */ +export interface SportmonksBaseParams { + apiKey: string + include?: string + filters?: string +} + +/** Pagination/ordering query parameters supported by paginated list endpoints. */ +export interface SportmonksPaginationParams { + per_page?: string + page?: string + order?: string +} + +/** + * Sportmonks v3 pagination metadata returned alongside paginated list responses. + * @see https://docs.sportmonks.com/v3/tutorials-and-guides/tutorials/introduction/pagination + */ +export interface SportmonksPagination { + count?: number + per_page?: number + current_page?: number + next_page?: string | null + has_more?: boolean +} + +/** Builds the auth headers for a Sportmonks request. */ +export function buildSportmonksHeaders(apiKey: string): Record { + return { + Authorization: apiKey, + Accept: 'application/json', + } +} + +/** Appends the shared Sportmonks query parameters (include, filters, pagination). */ +export function appendSportmonksQuery( + url: string, + params: SportmonksBaseParams & SportmonksPaginationParams +): string { + const query = new URLSearchParams() + if (params.include) query.append('include', params.include) + if (params.filters) query.append('filters', params.filters) + if (params.per_page) query.append('per_page', params.per_page) + if (params.page) query.append('page', params.page) + if (params.order) query.append('order', params.order) + const queryString = query.toString() + return queryString ? `${url}?${queryString}` : url +} + +/** Normalizes a Sportmonks error response into a thrown Error. */ +export function handleSportmonksError(data: any, status: number, operation: string): never { + const errorMessage = + data?.message || data?.error?.message || data?.error || `Unknown error during ${operation}` + throw new Error(`Sportmonks ${operation} failed (${status}): ${errorMessage}`) +} + +/** Output property definitions for the pagination metadata block. */ +export const SPORTMONKS_PAGINATION_PROPERTIES = { + count: { type: 'number', description: 'Number of results on the current page', optional: true }, + per_page: { type: 'number', description: 'Number of results per page', optional: true }, + current_page: { type: 'number', description: 'Current page number', optional: true }, + next_page: { + type: 'string', + description: 'URL of the next page of results', + nullable: true, + optional: true, + }, + has_more: { type: 'boolean', description: 'Whether more pages are available', optional: true }, +} as const satisfies Record + +/** Full pagination output definition reused across paginated list tools. */ +export const SPORTMONKS_PAGINATION_OUTPUT = { + type: 'object' as const, + description: 'Pagination metadata (present on paginated endpoints)', + optional: true, + properties: SPORTMONKS_PAGINATION_PROPERTIES, +} diff --git a/apps/sim/tools/sportmonks_core/get_cities.ts b/apps/sim/tools/sportmonks_core/get_cities.ts new file mode 100644 index 00000000000..5e09115e9d2 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_cities.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CITY_PROPERTIES, + SPORTMONKS_CORE_BASE_URL, + type SportmonksCity, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCitiesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetCitiesResponse extends ToolResponse { + output: { + cities: SportmonksCity[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreGetCitiesTool: ToolConfig< + SportmonksGetCitiesParams, + SportmonksGetCitiesResponse +> = { + id: 'sportmonks_core_get_cities', + name: 'Get Cities', + description: 'Retrieve all cities from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. region)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_CORE_BASE_URL}/cities`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_cities') + } + return { + success: true, + output: { + cities: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + cities: { + type: 'array', + description: 'Array of city objects', + items: { type: 'object', properties: SPORTMONKS_CITY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_city.ts b/apps/sim/tools/sportmonks_core/get_city.ts new file mode 100644 index 00000000000..93e8ff8dc27 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_city.ts @@ -0,0 +1,83 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CITY_PROPERTIES, + SPORTMONKS_CORE_BASE_URL, + type SportmonksCity, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCityParams extends SportmonksBaseParams { + cityId: string +} + +export interface SportmonksGetCityResponse extends ToolResponse { + output: { + city: SportmonksCity | null + } +} + +export const sportmonksCoreGetCityTool: ToolConfig< + SportmonksGetCityParams, + SportmonksGetCityResponse +> = { + id: 'sportmonks_core_get_city', + name: 'Get City by ID', + description: 'Retrieve a single city by its ID from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + cityId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the city', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. region)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/cities/${encodeURIComponent(params.cityId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_city') + } + return { + success: true, + output: { + city: data.data ?? null, + }, + } + }, + + outputs: { + city: { + type: 'object', + description: 'The requested city object', + properties: SPORTMONKS_CITY_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_continent.ts b/apps/sim/tools/sportmonks_core/get_continent.ts new file mode 100644 index 00000000000..4c362af5864 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_continent.ts @@ -0,0 +1,83 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CONTINENT_PROPERTIES, + SPORTMONKS_CORE_BASE_URL, + type SportmonksContinent, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetContinentParams extends SportmonksBaseParams { + continentId: string +} + +export interface SportmonksGetContinentResponse extends ToolResponse { + output: { + continent: SportmonksContinent | null + } +} + +export const sportmonksCoreGetContinentTool: ToolConfig< + SportmonksGetContinentParams, + SportmonksGetContinentResponse +> = { + id: 'sportmonks_core_get_continent', + name: 'Get Continent by ID', + description: 'Retrieve a single continent by its ID from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + continentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the continent', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. countries)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/continents/${encodeURIComponent(params.continentId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_continent') + } + return { + success: true, + output: { + continent: data.data ?? null, + }, + } + }, + + outputs: { + continent: { + type: 'object', + description: 'The requested continent object', + properties: SPORTMONKS_CONTINENT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_continents.ts b/apps/sim/tools/sportmonks_core/get_continents.ts new file mode 100644 index 00000000000..54c39b29452 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_continents.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CONTINENT_PROPERTIES, + SPORTMONKS_CORE_BASE_URL, + type SportmonksContinent, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetContinentsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetContinentsResponse extends ToolResponse { + output: { + continents: SportmonksContinent[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreGetContinentsTool: ToolConfig< + SportmonksGetContinentsParams, + SportmonksGetContinentsResponse +> = { + id: 'sportmonks_core_get_continents', + name: 'Get Continents', + description: 'Retrieve all continents from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. countries)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_CORE_BASE_URL}/continents`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_continents') + } + return { + success: true, + output: { + continents: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + continents: { + type: 'array', + description: 'Array of continent objects', + items: { type: 'object', properties: SPORTMONKS_CONTINENT_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_countries.ts b/apps/sim/tools/sportmonks_core/get_countries.ts new file mode 100644 index 00000000000..6d14938287a --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_countries.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_COUNTRY_PROPERTIES, + type SportmonksCountry, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCountriesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetCountriesResponse extends ToolResponse { + output: { + countries: SportmonksCountry[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreGetCountriesTool: ToolConfig< + SportmonksGetCountriesParams, + SportmonksGetCountriesResponse +> = { + id: 'sportmonks_core_get_countries', + name: 'Get Countries', + description: 'Retrieve all countries from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. continent;regions)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_CORE_BASE_URL}/countries`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_countries') + } + return { + success: true, + output: { + countries: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + countries: { + type: 'array', + description: 'Array of country objects', + items: { type: 'object', properties: SPORTMONKS_COUNTRY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_country.ts b/apps/sim/tools/sportmonks_core/get_country.ts new file mode 100644 index 00000000000..2171fd4ab9d --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_country.ts @@ -0,0 +1,83 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_COUNTRY_PROPERTIES, + type SportmonksCountry, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCountryParams extends SportmonksBaseParams { + countryId: string +} + +export interface SportmonksGetCountryResponse extends ToolResponse { + output: { + country: SportmonksCountry | null + } +} + +export const sportmonksCoreGetCountryTool: ToolConfig< + SportmonksGetCountryParams, + SportmonksGetCountryResponse +> = { + id: 'sportmonks_core_get_country', + name: 'Get Country by ID', + description: 'Retrieve a single country by its ID from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. continent;regions)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_country') + } + return { + success: true, + output: { + country: data.data ?? null, + }, + } + }, + + outputs: { + country: { + type: 'object', + description: 'The requested country object', + properties: SPORTMONKS_COUNTRY_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_region.ts b/apps/sim/tools/sportmonks_core/get_region.ts new file mode 100644 index 00000000000..fc6528f9c30 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_region.ts @@ -0,0 +1,83 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_REGION_PROPERTIES, + type SportmonksRegion, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRegionParams extends SportmonksBaseParams { + regionId: string +} + +export interface SportmonksGetRegionResponse extends ToolResponse { + output: { + region: SportmonksRegion | null + } +} + +export const sportmonksCoreGetRegionTool: ToolConfig< + SportmonksGetRegionParams, + SportmonksGetRegionResponse +> = { + id: 'sportmonks_core_get_region', + name: 'Get Region by ID', + description: 'Retrieve a single region by its ID from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + regionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the region', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;cities)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/regions/${encodeURIComponent(params.regionId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_region') + } + return { + success: true, + output: { + region: data.data ?? null, + }, + } + }, + + outputs: { + region: { + type: 'object', + description: 'The requested region object', + properties: SPORTMONKS_REGION_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_regions.ts b/apps/sim/tools/sportmonks_core/get_regions.ts new file mode 100644 index 00000000000..e7c279ab545 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_regions.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_REGION_PROPERTIES, + type SportmonksRegion, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRegionsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetRegionsResponse extends ToolResponse { + output: { + regions: SportmonksRegion[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreGetRegionsTool: ToolConfig< + SportmonksGetRegionsParams, + SportmonksGetRegionsResponse +> = { + id: 'sportmonks_core_get_regions', + name: 'Get Regions', + description: 'Retrieve all regions from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;cities)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_CORE_BASE_URL}/regions`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_regions') + } + return { + success: true, + output: { + regions: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + regions: { + type: 'array', + description: 'Array of region objects', + items: { type: 'object', properties: SPORTMONKS_REGION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_timezones.ts b/apps/sim/tools/sportmonks_core/get_timezones.ts new file mode 100644 index 00000000000..6782af2c230 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_timezones.ts @@ -0,0 +1,61 @@ +import { + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_CORE_BASE_URL } from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTimezonesParams extends SportmonksBaseParams {} + +export interface SportmonksGetTimezonesResponse extends ToolResponse { + output: { + timezones: string[] + } +} + +export const sportmonksCoreGetTimezonesTool: ToolConfig< + SportmonksGetTimezonesParams, + SportmonksGetTimezonesResponse +> = { + id: 'sportmonks_core_get_timezones', + name: 'Get Timezones', + description: 'Retrieve all supported time zones (IANA names) from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + }, + + request: { + url: () => `${SPORTMONKS_CORE_BASE_URL}/timezones`, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_timezones') + } + return { + success: true, + output: { + timezones: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + timezones: { + type: 'array', + description: 'Array of supported IANA time zone names (e.g. Europe/London)', + items: { type: 'string', description: 'IANA time zone name' }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_type.ts b/apps/sim/tools/sportmonks_core/get_type.ts new file mode 100644 index 00000000000..69a69e2eb79 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_type.ts @@ -0,0 +1,74 @@ +import { + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_TYPE_PROPERTIES, + type SportmonksType, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTypeParams extends SportmonksBaseParams { + typeId: string +} + +export interface SportmonksGetTypeResponse extends ToolResponse { + output: { + type: SportmonksType | null + } +} + +export const sportmonksCoreGetTypeTool: ToolConfig< + SportmonksGetTypeParams, + SportmonksGetTypeResponse +> = { + id: 'sportmonks_core_get_type', + name: 'Get Type by ID', + description: 'Retrieve a single type by its ID from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + typeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the type', + }, + }, + + request: { + url: (params) => + `${SPORTMONKS_CORE_BASE_URL}/types/${encodeURIComponent(params.typeId.trim())}`, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_type') + } + return { + success: true, + output: { + type: data.data ?? null, + }, + } + }, + + outputs: { + type: { + type: 'object', + description: 'The requested type object', + properties: SPORTMONKS_TYPE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_types.ts b/apps/sim/tools/sportmonks_core/get_types.ts new file mode 100644 index 00000000000..1035f1d6efe --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_types.ts @@ -0,0 +1,93 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_TYPE_PROPERTIES, + type SportmonksType, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTypesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetTypesResponse extends ToolResponse { + output: { + types: SportmonksType[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreGetTypesTool: ToolConfig< + SportmonksGetTypesParams, + SportmonksGetTypesResponse +> = { + id: 'sportmonks_core_get_types', + name: 'Get Types', + description: + 'Retrieve all types (reference data describing events, statistics, positions, etc.) from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_CORE_BASE_URL}/types`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_types') + } + return { + success: true, + output: { + types: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + types: { + type: 'array', + description: 'Array of type objects', + items: { type: 'object', properties: SPORTMONKS_TYPE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/index.ts b/apps/sim/tools/sportmonks_core/index.ts new file mode 100644 index 00000000000..1074d060463 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/index.ts @@ -0,0 +1,13 @@ +export { sportmonksCoreGetCitiesTool } from './get_cities' +export { sportmonksCoreGetCityTool } from './get_city' +export { sportmonksCoreGetContinentTool } from './get_continent' +export { sportmonksCoreGetContinentsTool } from './get_continents' +export { sportmonksCoreGetCountriesTool } from './get_countries' +export { sportmonksCoreGetCountryTool } from './get_country' +export { sportmonksCoreGetRegionTool } from './get_region' +export { sportmonksCoreGetRegionsTool } from './get_regions' +export { sportmonksCoreGetTimezonesTool } from './get_timezones' +export { sportmonksCoreGetTypeTool } from './get_type' +export { sportmonksCoreGetTypesTool } from './get_types' +export { sportmonksCoreSearchCitiesTool } from './search_cities' +export { sportmonksCoreSearchCountriesTool } from './search_countries' diff --git a/apps/sim/tools/sportmonks_core/search_cities.ts b/apps/sim/tools/sportmonks_core/search_cities.ts new file mode 100644 index 00000000000..91d463a5c41 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/search_cities.ts @@ -0,0 +1,103 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CITY_PROPERTIES, + SPORTMONKS_CORE_BASE_URL, + type SportmonksCity, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchCitiesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchCitiesResponse extends ToolResponse { + output: { + cities: SportmonksCity[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreSearchCitiesTool: ToolConfig< + SportmonksSearchCitiesParams, + SportmonksSearchCitiesResponse +> = { + id: 'sportmonks_core_search_cities', + name: 'Search Cities', + description: 'Search for cities by name from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The city name to search for (e.g. London)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. region)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/cities/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_cities') + } + return { + success: true, + output: { + cities: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + cities: { + type: 'array', + description: 'Array of city objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_CITY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/search_countries.ts b/apps/sim/tools/sportmonks_core/search_countries.ts new file mode 100644 index 00000000000..cf3fc3101b4 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/search_countries.ts @@ -0,0 +1,103 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_COUNTRY_PROPERTIES, + type SportmonksCountry, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchCountriesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchCountriesResponse extends ToolResponse { + output: { + countries: SportmonksCountry[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreSearchCountriesTool: ToolConfig< + SportmonksSearchCountriesParams, + SportmonksSearchCountriesResponse +> = { + id: 'sportmonks_core_search_countries', + name: 'Search Countries', + description: 'Search for countries by name from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The country name to search for (e.g. Brazil)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. continent)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/countries/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_countries') + } + return { + success: true, + output: { + countries: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + countries: { + type: 'array', + description: 'Array of country objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_COUNTRY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/types.ts b/apps/sim/tools/sportmonks_core/types.ts new file mode 100644 index 00000000000..08dd316f083 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/types.ts @@ -0,0 +1,172 @@ +import type { OutputProperty } from '@/tools/types' + +/** + * Base URL for the Sportmonks Core API v3 (shared reference data). + * @see https://docs.sportmonks.com/v3/core-api/core + */ +export const SPORTMONKS_CORE_BASE_URL = 'https://api.sportmonks.com/v3/core' + +/** + * Output property definitions for a Continent object. + * @see https://docs.sportmonks.com/v3/core-api/entities/core + */ +export const SPORTMONKS_CONTINENT_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the continent' }, + name: { type: 'string', description: 'Name of the continent' }, + code: { type: 'string', description: 'Short code of the continent', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Country object. + * @see https://docs.sportmonks.com/v3/core-api/entities/core + */ +export const SPORTMONKS_COUNTRY_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the country' }, + continent_id: { type: 'number', description: 'Continent of the country', nullable: true }, + name: { type: 'string', description: 'Name of the country' }, + official_name: { type: 'string', description: 'Official name of the country', optional: true }, + fifa_name: { + type: 'string', + description: 'Official FIFA short code name', + nullable: true, + optional: true, + }, + iso2: { type: 'string', description: 'Two letter country code', nullable: true, optional: true }, + iso3: { + type: 'string', + description: 'Three letter country code', + nullable: true, + optional: true, + }, + latitude: { + type: 'string', + description: 'Latitude position of the country', + nullable: true, + optional: true, + }, + longitude: { + type: 'string', + description: 'Longitude position of the country', + nullable: true, + optional: true, + }, + geonameid: { type: 'number', description: 'Official geonameid', nullable: true, optional: true }, + borders: { + type: 'array', + description: 'Neighbouring countries (ISO3 codes)', + nullable: true, + optional: true, + items: { type: 'string', description: 'ISO3 country code' }, + }, + image_path: { type: 'string', description: 'Image path to the country flag', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Region object. + * @see https://docs.sportmonks.com/v3/core-api/entities/core + */ +export const SPORTMONKS_REGION_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the region' }, + country_id: { type: 'number', description: 'Country of the region' }, + name: { type: 'string', description: 'Name of the region' }, +} as const satisfies Record + +/** + * Output property definitions for a City object. + * @see https://docs.sportmonks.com/v3/core-api/entities/core + */ +export const SPORTMONKS_CITY_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the city' }, + country_id: { type: 'number', description: 'Country of the city' }, + region: { type: 'number', description: 'Region of the city', nullable: true, optional: true }, + name: { type: 'string', description: 'Name of the city' }, + latitude: { type: 'string', description: 'Latitude of the city', nullable: true, optional: true }, + longitude: { + type: 'string', + description: 'Longitude of the city', + nullable: true, + optional: true, + }, + geonameid: { + type: 'number', + description: 'Official geonameid of the city', + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Type object. + * @see https://docs.sportmonks.com/v3/core-api/entities/core + */ +export const SPORTMONKS_TYPE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the type' }, + parent_id: { type: 'number', description: 'Parent type of the type', nullable: true }, + name: { type: 'string', description: 'Name of the type' }, + code: { type: 'string', description: 'Code of the type', nullable: true, optional: true }, + developer_name: { + type: 'string', + description: 'Developer name of the type', + nullable: true, + optional: true, + }, + group: { + type: 'string', + description: 'Group the type falls under', + nullable: true, + optional: true, + }, + description: { + type: 'string', + description: 'Description of the type', + nullable: true, + optional: true, + }, +} as const satisfies Record + +export interface SportmonksContinent { + id: number + name: string + code?: string +} + +export interface SportmonksCountry { + id: number + continent_id: number | null + name: string + official_name?: string + fifa_name?: string | null + iso2?: string | null + iso3?: string | null + latitude?: string | null + longitude?: string | null + geonameid?: number | null + borders?: string[] | null + image_path?: string +} + +export interface SportmonksRegion { + id: number + country_id: number + name: string +} + +export interface SportmonksCity { + id: number + country_id: number + region?: number | null + name: string + latitude?: string | null + longitude?: string | null + geonameid?: number | null +} + +export interface SportmonksType { + id: number + parent_id: number | null + name: string + code?: string | null + developer_name?: string | null + group?: string | null + description?: string | null +} diff --git a/apps/sim/tools/sportmonks_football/get_fixture.ts b/apps/sim/tools/sportmonks_football/get_fixture.ts new file mode 100644 index 00000000000..f81afecae27 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_fixture.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksGetFixtureResponse extends ToolResponse { + output: { + fixture: SportmonksFixture | null + } +} + +export const sportmonksGetFixtureTool: ToolConfig< + SportmonksGetFixtureParams, + SportmonksGetFixtureResponse +> = { + id: 'sportmonks_football_get_fixture', + name: 'Get Fixture by ID', + description: 'Retrieve a single football fixture by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores;events;lineups;statistics)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. eventTypes:14)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixture') + } + return { + success: true, + output: { + fixture: data.data ?? null, + }, + } + }, + + outputs: { + fixture: { + type: 'object', + description: 'The requested fixture object', + properties: SPORTMONKS_FIXTURE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_fixtures_by_date.ts b/apps/sim/tools/sportmonks_football/get_fixtures_by_date.ts new file mode 100644 index 00000000000..9e855825a21 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_fixtures_by_date.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetFixturesByDateParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + date: string +} + +export interface SportmonksGetFixturesByDateResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetFixturesByDateTool: ToolConfig< + SportmonksGetFixturesByDateParams, + SportmonksGetFixturesByDateResponse +> = { + id: 'sportmonks_football_get_fixtures_by_date', + name: 'Get Fixtures by Date', + description: 'Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + date: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The date to fetch fixtures for, in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501,271)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/date/${encodeURIComponent(params.date.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_date') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of fixture objects for the requested date', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_fixtures_by_date_range.ts b/apps/sim/tools/sportmonks_football/get_fixtures_by_date_range.ts new file mode 100644 index 00000000000..d38eca41ba8 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_fixtures_by_date_range.ts @@ -0,0 +1,126 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetFixturesByDateRangeParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + startDate: string + endDate: string +} + +export interface SportmonksGetFixturesByDateRangeResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetFixturesByDateRangeTool: ToolConfig< + SportmonksGetFixturesByDateRangeParams, + SportmonksGetFixturesByDateRangeResponse +> = { + id: 'sportmonks_football_get_fixtures_by_date_range', + name: 'Get Fixtures by Date Range', + description: + 'Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max range is 100 days.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + startDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYY-MM-DD format (max 100 days after start)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501,271)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/between/${encodeURIComponent( + params.startDate.trim() + )}/${encodeURIComponent(params.endDate.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_date_range') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of fixture objects within the requested date range', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_head_to_head.ts b/apps/sim/tools/sportmonks_football/get_head_to_head.ts new file mode 100644 index 00000000000..c67f996a1a2 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_head_to_head.ts @@ -0,0 +1,125 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetHeadToHeadParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + team1: string + team2: string +} + +export interface SportmonksGetHeadToHeadResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetHeadToHeadTool: ToolConfig< + SportmonksGetHeadToHeadParams, + SportmonksGetHeadToHeadResponse +> = { + id: 'sportmonks_football_get_head_to_head', + name: 'Get Head to Head', + description: 'Retrieve the head-to-head fixtures between two teams from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + team1: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The id of the first team', + }, + team2: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The id of the second team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/head-to-head/${encodeURIComponent( + params.team1.trim() + )}/${encodeURIComponent(params.team2.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_head_to_head') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of head-to-head fixture objects between the two teams', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_inplay_livescores.ts b/apps/sim/tools/sportmonks_football/get_inplay_livescores.ts new file mode 100644 index 00000000000..b594afb6eb0 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_inplay_livescores.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetInplayLivescoresParams extends SportmonksBaseParams {} + +export interface SportmonksGetInplayLivescoresResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + } +} + +export const sportmonksGetInplayLivescoresTool: ToolConfig< + SportmonksGetInplayLivescoresParams, + SportmonksGetInplayLivescoresResponse +> = { + id: 'sportmonks_football_get_inplay_livescores', + name: 'Get Inplay Livescores', + description: 'Retrieve all fixtures that are currently being played (in-play) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores;events)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/livescores/inplay`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_inplay_livescores') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of in-play fixture objects', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_league.ts b/apps/sim/tools/sportmonks_football/get_league.ts new file mode 100644 index 00000000000..969d17b90c9 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_league.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLeagueParams extends SportmonksBaseParams { + leagueId: string +} + +export interface SportmonksGetLeagueResponse extends ToolResponse { + output: { + league: SportmonksLeague | null + } +} + +export const sportmonksGetLeagueTool: ToolConfig< + SportmonksGetLeagueParams, + SportmonksGetLeagueResponse +> = { + id: 'sportmonks_football_get_league', + name: 'Get League by ID', + description: 'Retrieve a single football league by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + leagueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the league', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/${encodeURIComponent(params.leagueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_league') + } + return { + success: true, + output: { + league: data.data ?? null, + }, + } + }, + + outputs: { + league: { + type: 'object', + description: 'The requested league object', + properties: SPORTMONKS_LEAGUE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_leagues.ts b/apps/sim/tools/sportmonks_football/get_leagues.ts new file mode 100644 index 00000000000..dc66a7bc991 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_leagues.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLeaguesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetLeaguesResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLeaguesTool: ToolConfig< + SportmonksGetLeaguesParams, + SportmonksGetLeaguesResponse +> = { + id: 'sportmonks_football_get_leagues', + name: 'Get Leagues', + description: 'Retrieve all football leagues available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/leagues`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_livescores.ts b/apps/sim/tools/sportmonks_football/get_livescores.ts new file mode 100644 index 00000000000..4f20e769623 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_livescores.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLivescoresParams extends SportmonksBaseParams {} + +export interface SportmonksGetLivescoresResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + } +} + +export const sportmonksGetLivescoresTool: ToolConfig< + SportmonksGetLivescoresParams, + SportmonksGetLivescoresResponse +> = { + id: 'sportmonks_football_get_livescores', + name: 'Get Livescores', + description: + 'Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores;events)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/livescores`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_livescores') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of live fixture objects', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_player.ts b/apps/sim/tools/sportmonks_football/get_player.ts new file mode 100644 index 00000000000..301c0d61054 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_player.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PLAYER_PROPERTIES, + type SportmonksPlayer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPlayerParams extends SportmonksBaseParams { + playerId: string +} + +export interface SportmonksGetPlayerResponse extends ToolResponse { + output: { + player: SportmonksPlayer | null + } +} + +export const sportmonksGetPlayerTool: ToolConfig< + SportmonksGetPlayerParams, + SportmonksGetPlayerResponse +> = { + id: 'sportmonks_football_get_player', + name: 'Get Player by ID', + description: 'Retrieve a single football player by their ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + playerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the player', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;position;teams.team;statistics)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/players/${encodeURIComponent(params.playerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_player') + } + return { + success: true, + output: { + player: data.data ?? null, + }, + } + }, + + outputs: { + player: { + type: 'object', + description: 'The requested player object', + properties: SPORTMONKS_PLAYER_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_standings_by_season.ts b/apps/sim/tools/sportmonks_football/get_standings_by_season.ts new file mode 100644 index 00000000000..b1469fc6953 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_standings_by_season.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STANDING_PROPERTIES, + type SportmonksStanding, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStandingsBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetStandingsBySeasonResponse extends ToolResponse { + output: { + standings: SportmonksStanding[] + } +} + +export const sportmonksGetStandingsBySeasonTool: ToolConfig< + SportmonksGetStandingsBySeasonParams, + SportmonksGetStandingsBySeasonResponse +> = { + id: 'sportmonks_football_get_standings_by_season', + name: 'Get Standings by Season', + description: 'Retrieve the full league standings table for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details;form)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. standingStages:77453568)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/standings/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_standings_by_season') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of standing entries for the season', + items: { type: 'object', properties: SPORTMONKS_STANDING_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_team.ts b/apps/sim/tools/sportmonks_football/get_team.ts new file mode 100644 index 00000000000..7bc4e391114 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_team.ts @@ -0,0 +1,88 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_PROPERTIES, + type SportmonksTeam, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksGetTeamResponse extends ToolResponse { + output: { + team: SportmonksTeam | null + } +} + +export const sportmonksGetTeamTool: ToolConfig = + { + id: 'sportmonks_football_get_team', + name: 'Get Team by ID', + description: 'Retrieve a single football team by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;venue;coaches;players.player)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team') + } + return { + success: true, + output: { + team: data.data ?? null, + }, + } + }, + + outputs: { + team: { + type: 'object', + description: 'The requested team object', + properties: SPORTMONKS_TEAM_PROPERTIES, + }, + }, + } diff --git a/apps/sim/tools/sportmonks_football/get_team_squad.ts b/apps/sim/tools/sportmonks_football/get_team_squad.ts new file mode 100644 index 00000000000..c1a5c31d0a3 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_team_squad.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SQUAD_PROPERTIES, + type SportmonksSquadEntry, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamSquadParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksGetTeamSquadResponse extends ToolResponse { + output: { + squad: SportmonksSquadEntry[] + } +} + +export const sportmonksGetTeamSquadTool: ToolConfig< + SportmonksGetTeamSquadParams, + SportmonksGetTeamSquadResponse +> = { + id: 'sportmonks_football_get_team_squad', + name: 'Get Team Squad', + description: 'Retrieve the current domestic squad for a team by team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. player;position)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/squads/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_squad') + } + return { + success: true, + output: { + squad: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + squad: { + type: 'array', + description: 'Array of squad entries for the team', + items: { type: 'object', properties: SPORTMONKS_SQUAD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_topscorers_by_season.ts b/apps/sim/tools/sportmonks_football/get_topscorers_by_season.ts new file mode 100644 index 00000000000..036f909c575 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_topscorers_by_season.ts @@ -0,0 +1,117 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TOPSCORER_PROPERTIES, + type SportmonksTopscorer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTopscorersBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksGetTopscorersBySeasonResponse extends ToolResponse { + output: { + topscorers: SportmonksTopscorer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTopscorersBySeasonTool: ToolConfig< + SportmonksGetTopscorersBySeasonParams, + SportmonksGetTopscorersBySeasonResponse +> = { + id: 'sportmonks_football_get_topscorers_by_season', + name: 'Get Topscorers by Season', + description: + 'Retrieve the topscorers (goals, assists, cards) for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;participant;type)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. seasontopscorerTypes:208)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order topscorers by position (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/topscorers/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_topscorers_by_season') + } + return { + success: true, + output: { + topscorers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + topscorers: { + type: 'array', + description: 'Array of topscorer entries for the season', + items: { type: 'object', properties: SPORTMONKS_TOPSCORER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/index.ts b/apps/sim/tools/sportmonks_football/index.ts new file mode 100644 index 00000000000..1b07764325d --- /dev/null +++ b/apps/sim/tools/sportmonks_football/index.ts @@ -0,0 +1,15 @@ +export { sportmonksGetFixtureTool } from './get_fixture' +export { sportmonksGetFixturesByDateTool } from './get_fixtures_by_date' +export { sportmonksGetFixturesByDateRangeTool } from './get_fixtures_by_date_range' +export { sportmonksGetHeadToHeadTool } from './get_head_to_head' +export { sportmonksGetInplayLivescoresTool } from './get_inplay_livescores' +export { sportmonksGetLeagueTool } from './get_league' +export { sportmonksGetLeaguesTool } from './get_leagues' +export { sportmonksGetLivescoresTool } from './get_livescores' +export { sportmonksGetPlayerTool } from './get_player' +export { sportmonksGetStandingsBySeasonTool } from './get_standings_by_season' +export { sportmonksGetTeamTool } from './get_team' +export { sportmonksGetTeamSquadTool } from './get_team_squad' +export { sportmonksGetTopscorersBySeasonTool } from './get_topscorers_by_season' +export { sportmonksSearchPlayersTool } from './search_players' +export { sportmonksSearchTeamsTool } from './search_teams' diff --git a/apps/sim/tools/sportmonks_football/search_players.ts b/apps/sim/tools/sportmonks_football/search_players.ts new file mode 100644 index 00000000000..6987e4185ab --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_players.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PLAYER_PROPERTIES, + type SportmonksPlayer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchPlayersParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchPlayersResponse extends ToolResponse { + output: { + players: SportmonksPlayer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchPlayersTool: ToolConfig< + SportmonksSearchPlayersParams, + SportmonksSearchPlayersResponse +> = { + id: 'sportmonks_football_search_players', + name: 'Search Players', + description: 'Search for football players by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The player name to search for (e.g. Tavernier)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;position;teams.team)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order players by id (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/players/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_players') + } + return { + success: true, + output: { + players: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + players: { + type: 'array', + description: 'Array of player objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_PLAYER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_teams.ts b/apps/sim/tools/sportmonks_football/search_teams.ts new file mode 100644 index 00000000000..2a9eaf7507f --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_teams.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_PROPERTIES, + type SportmonksTeam, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchTeamsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchTeamsResponse extends ToolResponse { + output: { + teams: SportmonksTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchTeamsTool: ToolConfig< + SportmonksSearchTeamsParams, + SportmonksSearchTeamsResponse +> = { + id: 'sportmonks_football_search_teams', + name: 'Search Teams', + description: 'Search for football teams by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The team name to search for (e.g. Celtic)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;venue)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order teams by id (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/teams/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_teams') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/types.ts b/apps/sim/tools/sportmonks_football/types.ts new file mode 100644 index 00000000000..47343ec28f4 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/types.ts @@ -0,0 +1,392 @@ +import type { OutputProperty } from '@/tools/types' + +/** + * Base URL for the Sportmonks Football API v3. + * @see https://docs.sportmonks.com/v3/welcome/authentication + */ +export const SPORTMONKS_FOOTBALL_BASE_URL = 'https://api.sportmonks.com/v3/football' + +/** + * Output property definitions for a Fixture object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/fixture + */ +export const SPORTMONKS_FIXTURE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the fixture' }, + sport_id: { type: 'number', description: 'Sport the fixture is played at' }, + league_id: { type: 'number', description: 'League the fixture is played in' }, + season_id: { type: 'number', description: 'Season the fixture is played in' }, + stage_id: { type: 'number', description: 'Stage the fixture is played in' }, + group_id: { type: 'number', description: 'Group the fixture is played in', nullable: true }, + aggregate_id: { type: 'number', description: 'Aggregate the fixture belongs to', nullable: true }, + round_id: { type: 'number', description: 'Round the fixture is played in', nullable: true }, + state_id: { type: 'number', description: 'State (status) of the fixture' }, + venue_id: { type: 'number', description: 'Venue the fixture is played at', nullable: true }, + name: { type: 'string', description: 'Name of the fixture (participants)', nullable: true }, + starting_at: { type: 'string', description: 'Datetime the fixture starts', nullable: true }, + result_info: { + type: 'string', + description: 'Final result summary', + nullable: true, + optional: true, + }, + leg: { type: 'string', description: 'Leg of the fixture (e.g. 1/1)', optional: true }, + details: { + type: 'string', + description: 'Details about the fixture', + nullable: true, + optional: true, + }, + length: { + type: 'number', + description: 'Length of the fixture in minutes', + nullable: true, + optional: true, + }, + placeholder: { + type: 'boolean', + description: 'Whether the fixture is a placeholder', + optional: true, + }, + has_odds: { type: 'boolean', description: 'Whether odds are available', optional: true }, + has_premium_odds: { + type: 'boolean', + description: 'Whether premium odds are available', + optional: true, + }, + starting_at_timestamp: { + type: 'number', + description: 'UNIX timestamp of the start time', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Team object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/team-player-squad-coach-and-referee + */ +export const SPORTMONKS_TEAM_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the team' }, + sport_id: { type: 'number', description: 'Sport of the team' }, + country_id: { type: 'number', description: 'Country of the team' }, + venue_id: { + type: 'number', + description: 'Home venue of the team', + nullable: true, + optional: true, + }, + gender: { type: 'string', description: 'Gender of the team', optional: true }, + name: { type: 'string', description: 'Name of the team' }, + short_code: { + type: 'string', + description: 'Short code of the team', + nullable: true, + optional: true, + }, + image_path: { type: 'string', description: 'URL to the team logo', optional: true }, + founded: { + type: 'number', + description: 'Founding year of the team', + nullable: true, + optional: true, + }, + type: { type: 'string', description: 'Type of the team', optional: true }, + placeholder: { + type: 'boolean', + description: 'Whether the team is a placeholder', + optional: true, + }, + last_played_at: { + type: 'string', + description: 'Date and time of the last played match', + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Player object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/team-player-squad-coach-and-referee + */ +export const SPORTMONKS_PLAYER_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the player' }, + sport_id: { type: 'number', description: 'Sport of the player' }, + country_id: { type: 'number', description: 'Country of birth of the player', nullable: true }, + nationality_id: { type: 'number', description: 'Nationality of the player', nullable: true }, + city_id: { + type: 'number', + description: 'City of birth of the player', + nullable: true, + optional: true, + }, + position_id: { + type: 'number', + description: 'Position of the player', + nullable: true, + optional: true, + }, + detailed_position_id: { + type: 'number', + description: 'Detailed position of the player', + nullable: true, + optional: true, + }, + type_id: { type: 'number', description: 'Type of the player', nullable: true, optional: true }, + common_name: { type: 'string', description: 'Name the player is known for', optional: true }, + firstname: { type: 'string', description: 'First name of the player', optional: true }, + lastname: { type: 'string', description: 'Last name of the player', optional: true }, + name: { type: 'string', description: 'Name of the player' }, + display_name: { type: 'string', description: 'Display name of the player', optional: true }, + image_path: { type: 'string', description: 'URL to the player headshot', optional: true }, + height: { + type: 'number', + description: 'Height of the player in cm', + nullable: true, + optional: true, + }, + weight: { + type: 'number', + description: 'Weight of the player in kg', + nullable: true, + optional: true, + }, + date_of_birth: { + type: 'string', + description: 'Date of birth of the player', + nullable: true, + optional: true, + }, + gender: { type: 'string', description: 'Gender of the player', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a League object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/league-season-schedule-stage-and-round + */ +export const SPORTMONKS_LEAGUE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the league' }, + sport_id: { type: 'number', description: 'Sport of the league' }, + country_id: { type: 'number', description: 'Country of the league' }, + name: { type: 'string', description: 'Name of the league' }, + active: { + type: 'number', + description: 'Whether the league is active (1) or inactive (0)', + optional: true, + }, + short_code: { + type: 'string', + description: 'Short code of the league', + nullable: true, + optional: true, + }, + image_path: { type: 'string', description: 'URL to the league logo', optional: true }, + type: { type: 'string', description: 'Type of the league', optional: true }, + sub_type: { type: 'string', description: 'Subtype of the league', optional: true }, + last_played_at: { + type: 'string', + description: 'Date the last fixture was played', + nullable: true, + optional: true, + }, + category: { + type: 'number', + description: 'Importance category of the league (1-4)', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Standing object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/standing-and-topscorer + */ +export const SPORTMONKS_STANDING_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the standing' }, + participant_id: { type: 'number', description: 'Team related to the standing' }, + sport_id: { type: 'number', description: 'Sport related to the standing' }, + league_id: { type: 'number', description: 'League related to the standing' }, + season_id: { type: 'number', description: 'Season related to the standing' }, + stage_id: { type: 'number', description: 'Stage related to the standing' }, + group_id: { type: 'number', description: 'Group related to the standing', nullable: true }, + round_id: { type: 'number', description: 'Round related to the standing', nullable: true }, + standing_rule_id: { + type: 'number', + description: 'Standing rule related to the standing', + optional: true, + }, + position: { type: 'number', description: 'Position of the team in the standing' }, + result: { type: 'string', description: 'Movement of the team in the standing', optional: true }, + points: { type: 'number', description: 'Points the team has gathered' }, +} as const satisfies Record + +/** + * Output property definitions for a Topscorer object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/standing-and-topscorer + */ +export const SPORTMONKS_TOPSCORER_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the topscorer record' }, + season_id: { type: 'number', description: 'Season related to the topscorer' }, + league_id: { type: 'number', description: 'League related to the topscorer', optional: true }, + stage_id: { type: 'number', description: 'Stage related to the topscorer', optional: true }, + player_id: { type: 'number', description: 'Player related to the topscorer' }, + participant_id: { type: 'number', description: 'Team related to the topscorer' }, + type_id: { type: 'number', description: 'Type of the topscorer (goals, assists, cards)' }, + position: { type: 'number', description: 'Position of the topscorer' }, + total: { type: 'number', description: 'Number of goals, assists or cards' }, +} as const satisfies Record + +/** + * Output property definitions for a Team Squad entry. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/team-player-squad-coach-and-referee + */ +export const SPORTMONKS_SQUAD_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the squad record' }, + transfer_id: { + type: 'number', + description: 'Transfer id of the squad record', + nullable: true, + optional: true, + }, + player_id: { type: 'number', description: 'Player in the squad' }, + team_id: { type: 'number', description: 'Team of the squad' }, + position_id: { + type: 'number', + description: 'Position of the player in the squad', + nullable: true, + optional: true, + }, + detailed_position_id: { + type: 'number', + description: 'Detailed position of the player in the squad', + nullable: true, + optional: true, + }, + jersey_number: { + type: 'number', + description: 'Jersey number of the player', + nullable: true, + optional: true, + }, + start: { + type: 'string', + description: 'Start contract date of the player', + nullable: true, + optional: true, + }, + end: { + type: 'string', + description: 'End contract date of the player', + nullable: true, + optional: true, + }, +} as const satisfies Record + +export interface SportmonksFixture { + id: number + sport_id: number + league_id: number + season_id: number + stage_id: number + group_id: number | null + aggregate_id: number | null + round_id: number | null + state_id: number + venue_id: number | null + name: string | null + starting_at: string | null + result_info?: string | null + leg?: string + details?: string | null + length?: number | null + placeholder?: boolean + has_odds?: boolean + has_premium_odds?: boolean + starting_at_timestamp?: number +} + +export interface SportmonksTeam { + id: number + sport_id: number + country_id: number + venue_id?: number | null + gender?: string + name: string + short_code?: string | null + image_path?: string + founded?: number | null + type?: string + placeholder?: boolean + last_played_at?: string | null +} + +export interface SportmonksPlayer { + id: number + sport_id: number + country_id: number | null + nationality_id: number | null + city_id?: number | null + position_id?: number | null + detailed_position_id?: number | null + type_id?: number | null + common_name?: string + firstname?: string + lastname?: string + name: string + display_name?: string + image_path?: string + height?: number | null + weight?: number | null + date_of_birth?: string | null + gender?: string +} + +export interface SportmonksLeague { + id: number + sport_id: number + country_id: number + name: string + active?: number + short_code?: string | null + image_path?: string + type?: string + sub_type?: string + last_played_at?: string | null + category?: number +} + +export interface SportmonksStanding { + id: number + participant_id: number + sport_id: number + league_id: number + season_id: number + stage_id: number + group_id: number | null + round_id: number | null + standing_rule_id?: number + position: number + result?: string + points: number +} + +export interface SportmonksTopscorer { + id: number + season_id: number + league_id?: number + stage_id?: number + player_id: number + participant_id: number + type_id: number + position: number + total: number +} + +export interface SportmonksSquadEntry { + id: number + transfer_id?: number | null + player_id: number + team_id: number + position_id?: number | null + detailed_position_id?: number | null + jersey_number?: number | null + start?: string | null + end?: string | null +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_driver.ts b/apps/sim/tools/sportmonks_motorsport/get_driver.ts new file mode 100644 index 00000000000..0764250c619 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_driver.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_DRIVER_PROPERTIES, + type SportmonksMsDriver, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetDriverParams extends SportmonksBaseParams { + driverId: string +} + +export interface SportmonksMsGetDriverResponse extends ToolResponse { + output: { + driver: SportmonksMsDriver | null + } +} + +export const sportmonksMotorsportGetDriverTool: ToolConfig< + SportmonksMsGetDriverParams, + SportmonksMsGetDriverResponse +> = { + id: 'sportmonks_motorsport_get_driver', + name: 'Get Driver by ID', + description: 'Retrieve a single motorsport driver by their ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + driverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the driver', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/drivers/${encodeURIComponent(params.driverId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_driver') + } + return { + success: true, + output: { + driver: data.data ?? null, + }, + } + }, + + outputs: { + driver: { + type: 'object', + description: 'The requested driver object', + properties: SPORTMONKS_MS_DRIVER_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_driver_standings_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_driver_standings_by_season.ts new file mode 100644 index 00000000000..5f95c60228e --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_driver_standings_by_season.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STANDING_PROPERTIES, + type SportmonksMsStanding, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetDriverStandingsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetDriverStandingsResponse extends ToolResponse { + output: { + standings: SportmonksMsStanding[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetDriverStandingsBySeasonTool: ToolConfig< + SportmonksMsGetDriverStandingsParams, + SportmonksMsGetDriverStandingsResponse +> = { + id: 'sportmonks_motorsport_get_driver_standings_by_season', + name: 'Get Driver Standings by Season', + description: + 'Retrieve the drivers championship standings for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/standings/drivers/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_driver_standings_by_season') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of driver standing entries for the season', + items: { type: 'object', properties: SPORTMONKS_MS_STANDING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_drivers.ts b/apps/sim/tools/sportmonks_motorsport/get_drivers.ts new file mode 100644 index 00000000000..722a7e2742c --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_drivers.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_DRIVER_PROPERTIES, + type SportmonksMsDriver, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetDriversParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetDriversResponse extends ToolResponse { + output: { + drivers: SportmonksMsDriver[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetDriversTool: ToolConfig< + SportmonksMsGetDriversParams, + SportmonksMsGetDriversResponse +> = { + id: 'sportmonks_motorsport_get_drivers', + name: 'Get Drivers', + description: 'Retrieve all motorsport drivers from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/drivers`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_drivers') + } + return { + success: true, + output: { + drivers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + drivers: { + type: 'array', + description: 'Array of driver objects', + items: { type: 'object', properties: SPORTMONKS_MS_DRIVER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_fixture.ts new file mode 100644 index 00000000000..f62c25025ea --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_fixture.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetFixtureResponse extends ToolResponse { + output: { + fixture: SportmonksMsFixture | null + } +} + +export const sportmonksMotorsportGetFixtureTool: ToolConfig< + SportmonksMsGetFixtureParams, + SportmonksMsGetFixtureResponse +> = { + id: 'sportmonks_motorsport_get_fixture', + name: 'Get Motorsport Fixture by ID', + description: 'Retrieve a single motorsport fixture (session) by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;results;latestLaps;pitstops)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixture') + } + return { + success: true, + output: { + fixture: data.data ?? null, + }, + } + }, + + outputs: { + fixture: { + type: 'object', + description: 'The requested motorsport fixture (session) object', + properties: SPORTMONKS_MS_FIXTURE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date.ts b/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date.ts new file mode 100644 index 00000000000..6a3a620797c --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetFixturesByDateParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + date: string +} + +export interface SportmonksMsGetFixturesByDateResponse extends ToolResponse { + output: { + fixtures: SportmonksMsFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetFixturesByDateTool: ToolConfig< + SportmonksMsGetFixturesByDateParams, + SportmonksMsGetFixturesByDateResponse +> = { + id: 'sportmonks_motorsport_get_fixtures_by_date', + name: 'Get Motorsport Fixtures by Date', + description: + 'Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + date: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The date to fetch fixtures for, in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participants;venue)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/date/${encodeURIComponent(params.date.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_date') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of motorsport fixture (session) objects for the requested date', + items: { type: 'object', properties: SPORTMONKS_MS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture.ts new file mode 100644 index 00000000000..ffa114b8083 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLapsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetLapsByFixtureResponse extends ToolResponse { + output: { + laps: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetLapsByFixtureTool: ToolConfig< + SportmonksMsGetLapsByFixtureParams, + SportmonksMsGetLapsByFixtureResponse +> = { + id: 'sportmonks_motorsport_get_laps_by_fixture', + name: 'Get Laps by Fixture', + description: 'Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/laps` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_laps_by_fixture') + } + return { + success: true, + output: { + laps: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + laps: { + type: 'array', + description: 'Array of lap objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_livescores.ts b/apps/sim/tools/sportmonks_motorsport/get_livescores.ts new file mode 100644 index 00000000000..781430a89e8 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_livescores.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLivescoresParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetLivescoresResponse extends ToolResponse { + output: { + fixtures: SportmonksMsFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLivescoresTool: ToolConfig< + SportmonksMsGetLivescoresParams, + SportmonksMsGetLivescoresResponse +> = { + id: 'sportmonks_motorsport_get_livescores', + name: 'Get Motorsport Livescores', + description: 'Retrieve all live motorsport fixtures (sessions) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;results)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/livescores`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_livescores') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of live motorsport fixture (session) objects', + items: { type: 'object', properties: SPORTMONKS_MS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture.ts new file mode 100644 index 00000000000..367c0f079c2 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture.ts @@ -0,0 +1,91 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetPitstopsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetPitstopsByFixtureResponse extends ToolResponse { + output: { + pitstops: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetPitstopsByFixtureTool: ToolConfig< + SportmonksMsGetPitstopsByFixtureParams, + SportmonksMsGetPitstopsByFixtureResponse +> = { + id: 'sportmonks_motorsport_get_pitstops_by_fixture', + name: 'Get Pitstops by Fixture', + description: + 'Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/pitstops` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_pitstops_by_fixture') + } + return { + success: true, + output: { + pitstops: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + pitstops: { + type: 'array', + description: 'Array of pitstop objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_team.ts b/apps/sim/tools/sportmonks_motorsport/get_team.ts new file mode 100644 index 00000000000..e177e6c5624 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_team.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_TEAM_PROPERTIES, + type SportmonksMsTeam, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetTeamParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksMsGetTeamResponse extends ToolResponse { + output: { + team: SportmonksMsTeam | null + } +} + +export const sportmonksMotorsportGetTeamTool: ToolConfig< + SportmonksMsGetTeamParams, + SportmonksMsGetTeamResponse +> = { + id: 'sportmonks_motorsport_get_team', + name: 'Get Team by ID', + description: 'Retrieve a single motorsport team (constructor) by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team (constructor)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;drivers)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team') + } + return { + success: true, + output: { + team: data.data ?? null, + }, + } + }, + + outputs: { + team: { + type: 'object', + description: 'The requested team (constructor) object', + properties: SPORTMONKS_MS_TEAM_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_team_standings_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_team_standings_by_season.ts new file mode 100644 index 00000000000..7a6e90f9b9b --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_team_standings_by_season.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STANDING_PROPERTIES, + type SportmonksMsStanding, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetTeamStandingsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetTeamStandingsResponse extends ToolResponse { + output: { + standings: SportmonksMsStanding[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetTeamStandingsBySeasonTool: ToolConfig< + SportmonksMsGetTeamStandingsParams, + SportmonksMsGetTeamStandingsResponse +> = { + id: 'sportmonks_motorsport_get_team_standings_by_season', + name: 'Get Team Standings by Season', + description: + 'Retrieve the constructors championship standings for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/standings/teams/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_standings_by_season') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of team (constructor) standing entries for the season', + items: { type: 'object', properties: SPORTMONKS_MS_STANDING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_teams.ts b/apps/sim/tools/sportmonks_motorsport/get_teams.ts new file mode 100644 index 00000000000..137a2a5c4fb --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_teams.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_TEAM_PROPERTIES, + type SportmonksMsTeam, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetTeamsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetTeamsResponse extends ToolResponse { + output: { + teams: SportmonksMsTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetTeamsTool: ToolConfig< + SportmonksMsGetTeamsParams, + SportmonksMsGetTeamsResponse +> = { + id: 'sportmonks_motorsport_get_teams', + name: 'Get Teams', + description: 'Retrieve all motorsport teams (constructors) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;drivers)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/teams`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_teams') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team (constructor) objects', + items: { type: 'object', properties: SPORTMONKS_MS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/index.ts b/apps/sim/tools/sportmonks_motorsport/index.ts new file mode 100644 index 00000000000..acc0a9846ba --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/index.ts @@ -0,0 +1,12 @@ +export { sportmonksMotorsportGetDriverTool } from './get_driver' +export { sportmonksMotorsportGetDriverStandingsBySeasonTool } from './get_driver_standings_by_season' +export { sportmonksMotorsportGetDriversTool } from './get_drivers' +export { sportmonksMotorsportGetFixtureTool } from './get_fixture' +export { sportmonksMotorsportGetFixturesByDateTool } from './get_fixtures_by_date' +export { sportmonksMotorsportGetLapsByFixtureTool } from './get_laps_by_fixture' +export { sportmonksMotorsportGetLivescoresTool } from './get_livescores' +export { sportmonksMotorsportGetPitstopsByFixtureTool } from './get_pitstops_by_fixture' +export { sportmonksMotorsportGetTeamTool } from './get_team' +export { sportmonksMotorsportGetTeamStandingsBySeasonTool } from './get_team_standings_by_season' +export { sportmonksMotorsportGetTeamsTool } from './get_teams' +export { sportmonksMotorsportSearchDriversTool } from './search_drivers' diff --git a/apps/sim/tools/sportmonks_motorsport/search_drivers.ts b/apps/sim/tools/sportmonks_motorsport/search_drivers.ts new file mode 100644 index 00000000000..2680b01e0ff --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/search_drivers.ts @@ -0,0 +1,109 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_DRIVER_PROPERTIES, + type SportmonksMsDriver, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsSearchDriversParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksMsSearchDriversResponse extends ToolResponse { + output: { + drivers: SportmonksMsDriver[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportSearchDriversTool: ToolConfig< + SportmonksMsSearchDriversParams, + SportmonksMsSearchDriversResponse +> = { + id: 'sportmonks_motorsport_search_drivers', + name: 'Search Drivers', + description: 'Search for motorsport drivers by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The driver name to search for (e.g. Verstappen)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/drivers/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_drivers') + } + return { + success: true, + output: { + drivers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + drivers: { + type: 'array', + description: 'Array of driver objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_MS_DRIVER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/types.ts b/apps/sim/tools/sportmonks_motorsport/types.ts new file mode 100644 index 00000000000..51947e9e6ba --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/types.ts @@ -0,0 +1,317 @@ +import type { OutputProperty } from '@/tools/types' + +/** + * Base URL for the Sportmonks Motorsport API v3. + * @see https://docs.sportmonks.com/v3/motorsport-api/welcome/welcome + */ +export const SPORTMONKS_MOTORSPORT_BASE_URL = 'https://api.sportmonks.com/v3/motorsport' + +/** + * Output property definitions for a Motorsport Fixture (session) object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/fixture + */ +export const SPORTMONKS_MS_FIXTURE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the fixture (session)' }, + sport_id: { type: 'number', description: 'Sport of the fixture' }, + league_id: { type: 'number', description: 'League the fixture is held in' }, + season_id: { type: 'number', description: 'Season the fixture is held in' }, + stage_id: { type: 'number', description: 'Stage (race weekend) the fixture is held in' }, + group_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + aggregate_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + round_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + state_id: { type: 'number', description: 'State the fixture is currently in' }, + venue_id: { type: 'number', description: 'Venue (track) the fixture is held at', nullable: true }, + name: { + type: 'string', + description: 'Name of the fixture (e.g. Practice 1, Race)', + nullable: true, + }, + starting_at: { type: 'string', description: 'Start date and time', nullable: true }, + result_info: { type: 'string', description: 'Final result info', nullable: true, optional: true }, + leg: { + type: 'string', + description: 'Stage of the fixture (e.g. 2/3 for Practice 2)', + optional: true, + }, + details: { + type: 'string', + description: 'Details about the fixture', + nullable: true, + optional: true, + }, + length: { + type: 'number', + description: 'Session length in minutes or total laps', + nullable: true, + optional: true, + }, + placeholder: { + type: 'boolean', + description: 'Whether the fixture is a placeholder', + optional: true, + }, + has_odds: { type: 'boolean', description: 'Not used in the Motorsport API', optional: true }, + has_premium_odds: { + type: 'boolean', + description: 'Not used in the Motorsport API', + optional: true, + }, + starting_at_timestamp: { + type: 'number', + description: 'UNIX timestamp of the start time', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Driver object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/driver + */ +export const SPORTMONKS_MS_DRIVER_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the driver (player_id in responses)' }, + sport_id: { type: 'number', description: 'Sport of the driver' }, + country_id: { type: 'number', description: 'Country of birth of the driver', nullable: true }, + nationality_id: { type: 'number', description: 'Nationality of the driver', nullable: true }, + city_id: { + type: 'number', + description: 'City of birth of the driver', + nullable: true, + optional: true, + }, + position_id: { + type: 'number', + description: 'Position of the driver within the team', + nullable: true, + optional: true, + }, + detailed_position_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + type_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + common_name: { type: 'string', description: 'Name the driver is known for', optional: true }, + firstname: { type: 'string', description: 'First name of the driver', optional: true }, + lastname: { type: 'string', description: 'Last name of the driver', optional: true }, + name: { type: 'string', description: 'Name of the driver' }, + display_name: { type: 'string', description: 'Display name of the driver', optional: true }, + image_path: { type: 'string', description: 'URL to the driver headshot', optional: true }, + height: { + type: 'number', + description: 'Height of the driver in cm', + nullable: true, + optional: true, + }, + weight: { + type: 'number', + description: 'Weight of the driver in kg', + nullable: true, + optional: true, + }, + date_of_birth: { + type: 'string', + description: 'Date of birth of the driver', + nullable: true, + optional: true, + }, + gender: { type: 'string', description: 'Gender of the driver', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Motorsport Team (constructor) object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/team + */ +export const SPORTMONKS_MS_TEAM_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the team' }, + sport_id: { type: 'number', description: 'Sport of the team' }, + country_id: { type: 'number', description: 'Country of the team' }, + venue_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + gender: { type: 'string', description: 'Gender of the team', optional: true }, + name: { type: 'string', description: 'Name of the team (constructor)' }, + short_code: { + type: 'string', + description: 'Short code of the team', + nullable: true, + optional: true, + }, + image_path: { type: 'string', description: 'URL to the team logo', optional: true }, + founded: { + type: 'number', + description: 'Founding year of the team', + nullable: true, + optional: true, + }, + type: { type: 'string', description: 'Type of the team', optional: true }, + placeholder: { + type: 'boolean', + description: 'Whether the team is a placeholder', + optional: true, + }, + last_played_at: { + type: 'string', + description: "Date and time of the team's last session", + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Motorsport Standing object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/standing + */ +export const SPORTMONKS_MS_STANDING_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the standing' }, + participant_id: { type: 'number', description: 'Driver or team related to the standing' }, + sport_id: { type: 'number', description: 'Sport related to the standing' }, + league_id: { type: 'number', description: 'League related to the standing' }, + season_id: { type: 'number', description: 'Season related to the standing' }, + stage_id: { type: 'number', description: 'Stage related to the standing' }, + group_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + round_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + standing_rule_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + position: { type: 'number', description: 'Position of the participant in the standing' }, + result: { + type: 'string', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + points: { type: 'number', description: 'Points the participant has gathered' }, +} as const satisfies Record + +/** + * Output property definitions for a Lap / Pitstop object (identical shape). + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/lap + */ +export const SPORTMONKS_MS_LAP_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the lap/pitstop' }, + fixture_id: { type: 'number', description: 'Fixture related to the lap/pitstop' }, + lap_number: { type: 'number', description: 'Lap number in the fixture' }, + driver_number: { type: 'number', description: 'Number of the driver' }, + participant_id: { type: 'number', description: 'Driver related to the lap/pitstop' }, + is_latest: { type: 'boolean', description: 'Whether it is the latest lap/pitstop' }, +} as const satisfies Record + +export interface SportmonksMsFixture { + id: number + sport_id: number + league_id: number + season_id: number + stage_id: number + group_id?: number | null + aggregate_id?: number | null + round_id?: number | null + state_id: number + venue_id: number | null + name: string | null + starting_at: string | null + result_info?: string | null + leg?: string + details?: string | null + length?: number | null + placeholder?: boolean + has_odds?: boolean + has_premium_odds?: boolean + starting_at_timestamp?: number +} + +export interface SportmonksMsDriver { + id: number + sport_id: number + country_id: number | null + nationality_id: number | null + city_id?: number | null + position_id?: number | null + detailed_position_id?: number | null + type_id?: number | null + common_name?: string + firstname?: string + lastname?: string + name: string + display_name?: string + image_path?: string + height?: number | null + weight?: number | null + date_of_birth?: string | null + gender?: string +} + +export interface SportmonksMsTeam { + id: number + sport_id: number + country_id: number + venue_id?: number | null + gender?: string + name: string + short_code?: string | null + image_path?: string + founded?: number | null + type?: string + placeholder?: boolean + last_played_at?: string | null +} + +export interface SportmonksMsStanding { + id: number + participant_id: number + sport_id: number + league_id: number + season_id: number + stage_id: number + group_id?: number | null + round_id?: number | null + standing_rule_id?: number | null + position: number + result?: string | null + points: number +} + +export interface SportmonksMsLap { + id: number + fixture_id: number + lap_number: number + driver_number: number + participant_id: number + is_latest: boolean +} diff --git a/apps/sim/tools/sportmonks_odds/get_bookmaker.ts b/apps/sim/tools/sportmonks_odds/get_bookmaker.ts new file mode 100644 index 00000000000..b06ce7b7383 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_bookmaker.ts @@ -0,0 +1,74 @@ +import { + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_BOOKMAKER_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksBookmaker, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetBookmakerParams extends SportmonksBaseParams { + bookmakerId: string +} + +export interface SportmonksGetBookmakerResponse extends ToolResponse { + output: { + bookmaker: SportmonksBookmaker | null + } +} + +export const sportmonksOddsGetBookmakerTool: ToolConfig< + SportmonksGetBookmakerParams, + SportmonksGetBookmakerResponse +> = { + id: 'sportmonks_odds_get_bookmaker', + name: 'Get Bookmaker by ID', + description: 'Retrieve a single bookmaker by its ID from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + bookmakerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the bookmaker', + }, + }, + + request: { + url: (params) => + `${SPORTMONKS_ODDS_BASE_URL}/bookmakers/${encodeURIComponent(params.bookmakerId.trim())}`, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_bookmaker') + } + return { + success: true, + output: { + bookmaker: data.data ?? null, + }, + } + }, + + outputs: { + bookmaker: { + type: 'object', + description: 'The requested bookmaker object', + properties: SPORTMONKS_BOOKMAKER_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_bookmakers.ts b/apps/sim/tools/sportmonks_odds/get_bookmakers.ts new file mode 100644 index 00000000000..00d507d7728 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_bookmakers.ts @@ -0,0 +1,98 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_BOOKMAKER_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksBookmaker, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetBookmakersParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetBookmakersResponse extends ToolResponse { + output: { + bookmakers: SportmonksBookmaker[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetBookmakersTool: ToolConfig< + SportmonksGetBookmakersParams, + SportmonksGetBookmakersResponse +> = { + id: 'sportmonks_odds_get_bookmakers', + name: 'Get Bookmakers', + description: 'Retrieve all bookmakers from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. IdAfter:bookmakerID)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_ODDS_BASE_URL}/bookmakers`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_bookmakers') + } + return { + success: true, + output: { + bookmakers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + bookmakers: { + type: 'array', + description: 'Array of bookmaker objects', + items: { type: 'object', properties: SPORTMONKS_BOOKMAKER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts new file mode 100644 index 00000000000..72f610a6b93 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_INPLAY_ODD_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksInplayOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetInplayOddsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetInplayOddsResponse extends ToolResponse { + output: { + odds: SportmonksInplayOdd[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetInplayOddsByFixtureTool: ToolConfig< + SportmonksGetInplayOddsParams, + SportmonksGetInplayOddsResponse +> = { + id: 'sportmonks_odds_get_inplay_odds_by_fixture', + name: 'Get In-play Odds by Fixture', + description: + 'Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or bookmakers:2,14 or winningOdds)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_ODDS_BASE_URL}/inplay/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_inplay_odds_by_fixture') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of in-play odd objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_INPLAY_ODD_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_market.ts b/apps/sim/tools/sportmonks_odds/get_market.ts new file mode 100644 index 00000000000..06c974ab19e --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_market.ts @@ -0,0 +1,74 @@ +import { + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MARKET_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksMarket, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMarketParams extends SportmonksBaseParams { + marketId: string +} + +export interface SportmonksGetMarketResponse extends ToolResponse { + output: { + market: SportmonksMarket | null + } +} + +export const sportmonksOddsGetMarketTool: ToolConfig< + SportmonksGetMarketParams, + SportmonksGetMarketResponse +> = { + id: 'sportmonks_odds_get_market', + name: 'Get Market by ID', + description: 'Retrieve a single betting market by its ID from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + marketId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the market', + }, + }, + + request: { + url: (params) => + `${SPORTMONKS_ODDS_BASE_URL}/markets/${encodeURIComponent(params.marketId.trim())}`, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_market') + } + return { + success: true, + output: { + market: data.data ?? null, + }, + } + }, + + outputs: { + market: { + type: 'object', + description: 'The requested market object', + properties: SPORTMONKS_MARKET_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_markets.ts b/apps/sim/tools/sportmonks_odds/get_markets.ts new file mode 100644 index 00000000000..620756b00e1 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_markets.ts @@ -0,0 +1,98 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MARKET_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksMarket, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMarketsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetMarketsResponse extends ToolResponse { + output: { + markets: SportmonksMarket[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetMarketsTool: ToolConfig< + SportmonksGetMarketsParams, + SportmonksGetMarketsResponse +> = { + id: 'sportmonks_odds_get_markets', + name: 'Get Markets', + description: 'Retrieve all betting markets from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. IdAfter:marketID)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_ODDS_BASE_URL}/markets`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_markets') + } + return { + success: true, + output: { + markets: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + markets: { + type: 'array', + description: 'Array of market objects', + items: { type: 'object', properties: SPORTMONKS_MARKET_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts new file mode 100644 index 00000000000..f769d06bf63 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_ODD_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPreMatchOddsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetPreMatchOddsResponse extends ToolResponse { + output: { + odds: SportmonksOdd[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetPreMatchOddsByFixtureTool: ToolConfig< + SportmonksGetPreMatchOddsParams, + SportmonksGetPreMatchOddsResponse +> = { + id: 'sportmonks_odds_get_pre_match_odds_by_fixture', + name: 'Get Pre-match Odds by Fixture', + description: 'Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or bookmakers:2,14 or winningOdds)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_ODDS_BASE_URL}/pre-match/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_pre_match_odds_by_fixture') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of pre-match odd objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_ODD_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/index.ts b/apps/sim/tools/sportmonks_odds/index.ts new file mode 100644 index 00000000000..ec81b21556e --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/index.ts @@ -0,0 +1,8 @@ +export { sportmonksOddsGetBookmakerTool } from './get_bookmaker' +export { sportmonksOddsGetBookmakersTool } from './get_bookmakers' +export { sportmonksOddsGetInplayOddsByFixtureTool } from './get_inplay_odds_by_fixture' +export { sportmonksOddsGetMarketTool } from './get_market' +export { sportmonksOddsGetMarketsTool } from './get_markets' +export { sportmonksOddsGetPreMatchOddsByFixtureTool } from './get_pre_match_odds_by_fixture' +export { sportmonksOddsSearchBookmakersTool } from './search_bookmakers' +export { sportmonksOddsSearchMarketsTool } from './search_markets' diff --git a/apps/sim/tools/sportmonks_odds/search_bookmakers.ts b/apps/sim/tools/sportmonks_odds/search_bookmakers.ts new file mode 100644 index 00000000000..dc03a8fafff --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/search_bookmakers.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_BOOKMAKER_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksBookmaker, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchBookmakersParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchBookmakersResponse extends ToolResponse { + output: { + bookmakers: SportmonksBookmaker[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsSearchBookmakersTool: ToolConfig< + SportmonksSearchBookmakersParams, + SportmonksSearchBookmakersResponse +> = { + id: 'sportmonks_odds_search_bookmakers', + name: 'Search Bookmakers', + description: 'Search for bookmakers by name from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The bookmaker name to search for (e.g. bet365)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_ODDS_BASE_URL}/bookmakers/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_bookmakers') + } + return { + success: true, + output: { + bookmakers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + bookmakers: { + type: 'array', + description: 'Array of bookmaker objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_BOOKMAKER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/search_markets.ts b/apps/sim/tools/sportmonks_odds/search_markets.ts new file mode 100644 index 00000000000..dd85ef536e7 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/search_markets.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MARKET_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksMarket, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchMarketsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchMarketsResponse extends ToolResponse { + output: { + markets: SportmonksMarket[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsSearchMarketsTool: ToolConfig< + SportmonksSearchMarketsParams, + SportmonksSearchMarketsResponse +> = { + id: 'sportmonks_odds_search_markets', + name: 'Search Markets', + description: 'Search for betting markets by name from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The market name to search for (e.g. Over/Under)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_ODDS_BASE_URL}/markets/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_markets') + } + return { + success: true, + output: { + markets: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + markets: { + type: 'array', + description: 'Array of market objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_MARKET_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/types.ts b/apps/sim/tools/sportmonks_odds/types.ts new file mode 100644 index 00000000000..90fbdadf7ab --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/types.ts @@ -0,0 +1,230 @@ +import type { OutputProperty } from '@/tools/types' + +/** + * Base URL for the Sportmonks Odds API v3. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/welcome + */ +export const SPORTMONKS_ODDS_BASE_URL = 'https://api.sportmonks.com/v3/odds' + +/** + * Output property definitions for a pre-match Odd object. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/odd + */ +export const SPORTMONKS_ODD_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the odd' }, + fixture_id: { type: 'number', description: 'Fixture the odd belongs to' }, + market_id: { type: 'number', description: 'Market the odd belongs to' }, + bookmaker_id: { type: 'number', description: 'Bookmaker offering the odd' }, + label: { type: 'string', description: 'Outcome label (e.g. 1, X, 2)', nullable: true }, + value: { type: 'string', description: 'Decimal odds value', nullable: true }, + name: { type: 'string', description: 'Outcome name (e.g. Home, Draw, Away)', nullable: true }, + sort_order: { + type: 'number', + description: 'Sort order of the odd', + nullable: true, + optional: true, + }, + market_description: { + type: 'string', + description: 'Description of the market', + nullable: true, + optional: true, + }, + probability: { + type: 'string', + description: 'Implied probability (e.g. 48.78%)', + nullable: true, + optional: true, + }, + dp3: { + type: 'string', + description: 'Decimal odds to 3 decimal places', + nullable: true, + optional: true, + }, + fractional: { + type: 'string', + description: 'Fractional odds (e.g. 31/15)', + nullable: true, + optional: true, + }, + american: { + type: 'string', + description: 'American/moneyline odds (e.g. +104)', + nullable: true, + optional: true, + }, + winning: { + type: 'boolean', + description: 'Whether this is the winning outcome', + nullable: true, + optional: true, + }, + stopped: { + type: 'boolean', + description: 'Whether the odd is stopped', + nullable: true, + optional: true, + }, + total: { + type: 'string', + description: 'Total line for over/under markets', + nullable: true, + optional: true, + }, + handicap: { + type: 'string', + description: 'Handicap line for handicap markets', + nullable: true, + optional: true, + }, + participants: { + type: 'string', + description: 'Participant ids related to the outcome', + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for an in-play Odd object. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/inplayodd + */ +export const SPORTMONKS_INPLAY_ODD_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the odd' }, + fixture_id: { type: 'number', description: 'Fixture the odd belongs to' }, + external_id: { + type: 'number', + description: 'External id of the odd', + nullable: true, + optional: true, + }, + market_id: { type: 'number', description: 'Market the odd belongs to' }, + bookmaker_id: { type: 'number', description: 'Bookmaker offering the odd' }, + label: { type: 'string', description: 'Outcome label (e.g. 1, X, 2)', nullable: true }, + value: { type: 'string', description: 'Decimal odds value', nullable: true }, + name: { type: 'string', description: 'Outcome name', nullable: true }, + sort_order: { + type: 'number', + description: 'Sort order of the odd', + nullable: true, + optional: true, + }, + market_description: { + type: 'string', + description: 'Description of the market', + nullable: true, + optional: true, + }, + probability: { + type: 'string', + description: 'Implied probability', + nullable: true, + optional: true, + }, + dp3: { + type: 'string', + description: 'Decimal odds to 3 decimal places', + nullable: true, + optional: true, + }, + fractional: { type: 'string', description: 'Fractional odds', nullable: true, optional: true }, + american: { + type: 'string', + description: 'American/moneyline odds', + nullable: true, + optional: true, + }, + winning: { + type: 'boolean', + description: 'Whether this is the winning outcome', + nullable: true, + optional: true, + }, + suspended: { + type: 'boolean', + description: 'Whether the odd is suspended', + nullable: true, + optional: true, + }, + stopped: { + type: 'boolean', + description: 'Whether the odd is stopped', + nullable: true, + optional: true, + }, + total: { + type: 'string', + description: 'Total line for over/under markets', + nullable: true, + optional: true, + }, + handicap: { + type: 'string', + description: 'Handicap line for handicap markets', + nullable: true, + optional: true, + }, + participants: { + type: 'string', + description: 'Participant ids related to the outcome', + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Bookmaker object. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/bookmaker + */ +export const SPORTMONKS_BOOKMAKER_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the bookmaker' }, + name: { type: 'string', description: 'Name of the bookmaker' }, + logo: { type: 'string', description: 'Logo of the bookmaker', nullable: true, optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Market object. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/market + */ +export const SPORTMONKS_MARKET_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the market' }, + name: { type: 'string', description: 'Name of the market' }, +} as const satisfies Record + +export interface SportmonksOdd { + id: number + fixture_id: number + market_id: number + bookmaker_id: number + label: string | null + value: string | null + name: string | null + sort_order?: number | null + market_description?: string | null + probability?: string | null + dp3?: string | null + fractional?: string | null + american?: string | null + winning?: boolean | null + stopped?: boolean | null + total?: string | null + handicap?: string | null + participants?: string | null +} + +export interface SportmonksInplayOdd extends SportmonksOdd { + external_id?: number | null + suspended?: boolean | null +} + +export interface SportmonksBookmaker { + id: number + name: string + logo?: string | null +} + +export interface SportmonksMarket { + id: number + name: string +} From ea83be3bbe3199da4f554f5c0099c97b2c6abcca Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:58:38 -0700 Subject: [PATCH 02/16] fix(mship): add folder rename tools and locked workflow status (#5126) * fix(mship): add folder rename tools and locked workflow status * fix(mship): manage_folder bug fixes * improvement(mship): clean up deprecated fields from contracts * test(mship): update tool-call display title tests for client-derived titles Display titles now come from the Sim-side name resolver, not the stream's ui.title/phaseLabel. Update the read lifecycle test to expect the name-derived title and drop the obsolete phaseLabel-fallback test. * fix(contracts): lint --- .../message-content/message-content.tsx | 18 +- .../home/hooks/stream/stream-helpers.ts | 162 +----- .../home/hooks/stream/turn-model.ts | 4 +- .../app/workspace/[workspaceId]/home/types.ts | 79 --- .../lib/copilot/chat/effective-transcript.ts | 16 +- .../generated/mothership-stream-v1-schema.ts | 15 - .../copilot/generated/mothership-stream-v1.ts | 5 - .../lib/copilot/generated/tool-catalog-v1.ts | 174 +++--- .../lib/copilot/generated/tool-schemas-v1.ts | 106 ++-- .../copilot/request/handlers/handlers.test.ts | 50 +- apps/sim/lib/copilot/request/handlers/tool.ts | 28 +- .../sim/lib/copilot/request/handlers/types.ts | 11 +- .../tool-executor/register-handlers.ts | 15 +- .../lib/copilot/tools/handlers/param-types.ts | 13 +- .../tools/handlers/workflow/mutations.ts | 152 +++++ .../tools/handlers/workflow/queries.ts | 31 +- apps/sim/lib/copilot/tools/mcp/definitions.ts | 535 ------------------ apps/sim/lib/copilot/tools/tool-display.ts | 223 ++++++++ apps/sim/lib/copilot/vfs/serializers.ts | 41 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 35 +- apps/sim/lib/workflows/utils.ts | 1 + 21 files changed, 617 insertions(+), 1097 deletions(-) delete mode 100644 apps/sim/lib/copilot/tools/mcp/definitions.ts create mode 100644 apps/sim/lib/copilot/tools/tool-display.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 356ea7e00ad..a8a0ae35b9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -1,14 +1,14 @@ 'use client' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { stripVersionSuffix } from '@sim/utils/string' import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' +import { getToolDisplayTitle, humanizeToolName } from '@/lib/copilot/tools/tool-display' import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import type { ContentBlock, OptionItem, ToolCallData } from '../../types' -import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' +import { SUBAGENT_LABELS } from '../../types' import type { AgentGroupItem } from './components' import { AgentGroup, @@ -84,16 +84,9 @@ function isHiddenToolCall(toolName: string | undefined): boolean { return isToolHiddenInUi(toolName) } -function formatToolName(name: string): string { - return stripVersionSuffix(name) - .split('_') - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' ') -} - function resolveAgentLabel(key: string): string { if (key === 'mothership') return 'Sim' - return SUBAGENT_LABELS[key] ?? formatToolName(key) + return SUBAGENT_LABELS[key] ?? humanizeToolName(key) } function isDelegatingTool(tc: NonNullable): boolean { @@ -129,10 +122,7 @@ function getOverrideDisplayTitle(tc: NonNullable): str function toToolData(tc: NonNullable): ToolCallData { const overrideDisplayTitle = getOverrideDisplayTitle(tc) const displayTitle = - overrideDisplayTitle || - tc.displayTitle || - TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title || - formatToolName(tc.name) + overrideDisplayTitle || tc.displayTitle || getToolDisplayTitle(tc.name, tc.params) return { id: tc.id, diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index 9560560e1b9..72e1bd2dabc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -2,27 +2,25 @@ import { createLogger } from '@sim/logger' import { isRecordLike } from '@sim/utils/object' import { CrawlWebsite, - CreateFolder, - DeleteFolder, DeleteWorkflow, DeployApi, DeployChat, DeployMcp, FunctionExecute, - GetPageContents, Glob, Grep, ManageCredential, ManageCredentialOperation, ManageCustomTool, ManageCustomToolOperation, + ManageFolder, + ManageFolderOperation, ManageMcpTool, ManageMcpToolOperation, ManageScheduledTask, ManageScheduledTaskOperation, ManageSkill, ManageSkillOperation, - MoveFolder, MoveWorkflow, QueryLogs, Redeploy, @@ -36,6 +34,7 @@ import { WorkspaceFileOperation, } from '@/lib/copilot/generated/tool-catalog-v1' import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types' +import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display' import type { ContentBlock, MothershipResource } from '@/app/workspace/[workspaceId]/home/types' import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache' @@ -53,11 +52,7 @@ export const DEPLOY_TOOL_NAMES: Set = new Set([ Redeploy.id, ]) -export const FOLDER_TOOL_NAMES: Set = new Set([ - CreateFolder.id, - DeleteFolder.id, - MoveFolder.id, -]) +export const FOLDER_TOOL_NAMES: Set = new Set([ManageFolder.id]) export const WORKFLOW_MUTATION_TOOL_NAMES: Set = new Set([ MoveWorkflow.id, @@ -160,11 +155,6 @@ function stringParam(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined } -function stringArrayParam(value: unknown): string[] { - if (!Array.isArray(value)) return [] - return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) -} - function resolveWorkflowNameForDisplay(workflowId: unknown): string | undefined { const id = stringParam(workflowId) if (!id) return undefined @@ -218,125 +208,18 @@ function functionExecuteTitle(title: string | undefined): string { return title ?? 'Running code' } -export function resolveToolDisplayTitle( - name: string, - args?: Record -): string | undefined { - if (!args) return undefined - - if (name === FunctionExecute.id) { - return functionExecuteTitle(stringParam(args.title)) - } - - if (name === WorkspaceFile.id) { - const target = asPayloadRecord(args.target) - return resolveWorkspaceFileDisplayTitle(args.operation, args.title, target?.fileName) - } - - if (name === SearchOnline.id) { - const toolTitle = stringParam(args.toolTitle) - return toolTitle ? `Searching online for ${toolTitle}` : 'Searching online' - } - - if (name === Grep.id) { - const toolTitle = stringParam(args.toolTitle) - return toolTitle ? `Searching for ${toolTitle}` : 'Searching' - } - - if (name === Glob.id) { - const toolTitle = stringParam(args.toolTitle) - return toolTitle ? `Finding ${toolTitle}` : 'Finding files' - } - - if (name === ScrapePage.id) { - const url = stringParam(args.url) - return url ? `Scraping ${url}` : 'Scraping page' - } - - if (name === CrawlWebsite.id) { - const url = stringParam(args.url) - return url ? `Crawling ${url}` : 'Crawling website' - } - - if (name === GetPageContents.id) { - const urls = stringArrayParam(args.urls) - if (urls.length === 1) return `Getting ${urls[0]}` - if (urls.length > 1) return `Getting ${urls.length} pages` - return 'Getting page contents' - } - - if (name === ManageCustomTool.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageCustomToolOperation.add]: 'Creating custom tool', - [ManageCustomToolOperation.edit]: 'Updating custom tool', - [ManageCustomToolOperation.delete]: 'Deleting custom tool', - [ManageCustomToolOperation.list]: 'Listing custom tools', - }, - 'Custom tool action' - ) - } - - if (name === ManageMcpTool.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageMcpToolOperation.add]: 'Creating MCP server', - [ManageMcpToolOperation.edit]: 'Updating MCP server', - [ManageMcpToolOperation.delete]: 'Deleting MCP server', - [ManageMcpToolOperation.list]: 'Listing MCP servers', - }, - 'MCP server action' - ) - } - - if (name === ManageSkill.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageSkillOperation.add]: 'Creating skill', - [ManageSkillOperation.edit]: 'Updating skill', - [ManageSkillOperation.delete]: 'Deleting skill', - [ManageSkillOperation.list]: 'Listing skills', - }, - 'Skill action' - ) - } - - if (name === ManageScheduledTask.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageScheduledTaskOperation.create]: 'Creating scheduled task', - [ManageScheduledTaskOperation.get]: 'Getting scheduled task', - [ManageScheduledTaskOperation.update]: 'Updating scheduled task', - [ManageScheduledTaskOperation.delete]: 'Deleting scheduled task', - [ManageScheduledTaskOperation.list]: 'Listing scheduled tasks', - }, - 'Scheduled task action' - ) - } - - if (name === ManageCredential.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageCredentialOperation.rename]: 'Renaming credential', - [ManageCredentialOperation.delete]: 'Deleting credential', - }, - 'Credential action' - ) - } - +export function resolveToolDisplayTitle(name: string, args?: Record): string { + // Cases that enrich the title with live workspace/block names from the client + // stores. Everything else is resolved by the shared name+args resolver, which + // is the single source of truth for tool-call titles. if (name === RunWorkflow.id) { - const workflowName = resolveWorkflowNameForDisplay(args.workflowId) + const workflowName = resolveWorkflowNameForDisplay(args?.workflowId) return workflowName ? `Running ${workflowName}` : 'Running workflow' } if (name === RunFromBlock.id) { - const workflowName = resolveWorkflowNameForDisplay(args.workflowId) - const blockName = resolveBlockNameForDisplay(args.startBlockId) + const workflowName = resolveWorkflowNameForDisplay(args?.workflowId) + const blockName = resolveBlockNameForDisplay(args?.startBlockId) if (workflowName && blockName) return `Running ${workflowName} from ${blockName}` if (workflowName) return `Running ${workflowName}` if (blockName) return `Running from ${blockName}` @@ -344,8 +227,8 @@ export function resolveToolDisplayTitle( } if (name === RunWorkflowUntilBlock.id) { - const workflowName = resolveWorkflowNameForDisplay(args.workflowId) - const blockName = resolveBlockNameForDisplay(args.stopAfterBlockId) + const workflowName = resolveWorkflowNameForDisplay(args?.workflowId) + const blockName = resolveBlockNameForDisplay(args?.stopAfterBlockId) if (workflowName && blockName) return `Running ${workflowName} until ${blockName}` if (workflowName) return `Running ${workflowName}` if (blockName) return `Running until ${blockName}` @@ -354,11 +237,11 @@ export function resolveToolDisplayTitle( if (name === QueryLogs.id) { const workflowName = - resolveWorkflowNameForDisplay(args.workflowId) ?? stringParam(args.workflowName) - return workflowName ? `Querying logs for ${workflowName}` : undefined + resolveWorkflowNameForDisplay(args?.workflowId) ?? stringParam(args?.workflowName) + if (workflowName) return `Querying logs for ${workflowName}` } - return undefined + return getToolDisplayTitle(name, args) } function decodeStreamingString(value: string): string { @@ -480,5 +363,18 @@ export function resolveStreamingToolDisplayTitle( ) } + if (name === ManageFolder.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageFolderOperation.create]: 'Creating folder', + [ManageFolderOperation.rename]: 'Renaming folder', + [ManageFolderOperation.move]: 'Moving folder', + [ManageFolderOperation.delete]: 'Deleting folder', + }, + 'Folder action' + ) + } + return undefined } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts index 3a2fd46e400..11acb17ec32 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts @@ -477,9 +477,9 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve tsMs ) if (isRecord(payload.arguments)) node.args = payload.arguments + // Tool-call titles are derived from the tool name (+args) at serialize + // time; the stream only carries behavioral flags now. const ui = isRecord(payload.ui) ? payload.ui : undefined - const uiTitle = ui ? (asString(ui.title) ?? asString(ui.phaseLabel)) : undefined - if (uiTitle) node.uiTitle = uiTitle if (ui?.hidden === true) node.hidden = true } else if (phase === MothershipStreamV1ToolPhase.args_delta) { const node = upsertToolNode( diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 531a5a18639..20bc2513dab 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -1,36 +1,3 @@ -import { - Agent, - Auth, - CreateWorkflow, - Deploy, - EditWorkflow, - Ffmpeg, - FunctionExecute, - GenerateAudio, - GenerateImage, - GenerateVideo, - GetPageContents, - Glob, - Grep, - Knowledge, - KnowledgeBase, - ManageMcpTool, - ManageSkill, - Media, - OpenResource, - Read as ReadTool, - Research, - ScheduledTask, - ScrapePage, - SearchLibraryDocs, - SearchOnline, - Superagent, - Table, - UserMemory, - UserTable, - Workflow, - WorkspaceFile, -} from '@/lib/copilot/generated/tool-catalog-v1' import type { ChatContext } from '@/stores/panel' const EDIT_CONTENT_TOOL_ID = 'edit_content' @@ -199,49 +166,3 @@ export const SUBAGENT_LABELS: Record = { file: 'File Agent', media: 'Media Agent', } as const - -interface ToolTitleMetadata { - title: string -} - -/** - * Fallback titles for tool calls when the stream did not provide one. - */ -export const TOOL_UI_METADATA: Record = { - [Glob.id]: { title: 'Finding files' }, - [Grep.id]: { title: 'Searching' }, - [ReadTool.id]: { title: 'Reading file' }, - [SearchOnline.id]: { title: 'Searching online' }, - [ScrapePage.id]: { title: 'Scraping page' }, - [GetPageContents.id]: { title: 'Getting page contents' }, - [SearchLibraryDocs.id]: { title: 'Searching library docs' }, - [ManageMcpTool.id]: { title: 'MCP server action' }, - [ManageSkill.id]: { title: 'Skill action' }, - [UserMemory.id]: { title: 'Accessing memory' }, - [FunctionExecute.id]: { title: 'Running code' }, - [Superagent.id]: { title: 'Executing action' }, - [UserTable.id]: { title: 'Managing table' }, - [WorkspaceFile.id]: { title: 'Editing file' }, - [EDIT_CONTENT_TOOL_ID]: { title: 'Applying file content' }, - [CreateWorkflow.id]: { title: 'Creating workflow' }, - [EditWorkflow.id]: { title: 'Editing workflow' }, - [Workflow.id]: { title: 'Workflow Agent' }, - [RUN_SUBAGENT_ID]: { title: 'Run Agent' }, - [Deploy.id]: { title: 'Deploy Agent' }, - [Auth.id]: { title: 'Auth Agent' }, - [Knowledge.id]: { title: 'Knowledge Agent' }, - [KnowledgeBase.id]: { title: 'Managing knowledge base' }, - [Table.id]: { title: 'Table Agent' }, - [ScheduledTask.id]: { title: 'Scheduled Task Agent' }, - job: { title: 'Job Agent' }, - [Agent.id]: { title: 'Tools Agent' }, - custom_tool: { title: 'Creating tool' }, - [Research.id]: { title: 'Research Agent' }, - [OpenResource.id]: { title: 'Opening resource' }, - [Media.id]: { title: 'Media Agent' }, - [GenerateImage.id]: { title: 'Generating image' }, - [GenerateVideo.id]: { title: 'Generating video' }, - [GenerateAudio.id]: { title: 'Generating audio' }, - [Ffmpeg.id]: { title: 'Processing media' }, - context_compaction: { title: 'Compacted context' }, -} diff --git a/apps/sim/lib/copilot/chat/effective-transcript.ts b/apps/sim/lib/copilot/chat/effective-transcript.ts index f615448a356..ae971047208 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.ts @@ -14,6 +14,7 @@ import { } from '@/lib/copilot/generated/mothership-stream-v1' import type { FilePreviewSession } from '@/lib/copilot/request/session/file-preview-session-contract' import type { StreamBatchEvent } from '@/lib/copilot/request/session/types' +import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display' interface StreamSnapshotLike { events: StreamBatchEvent[] @@ -60,15 +61,6 @@ function buildInlineErrorTag(payload: MothershipStreamV1ErrorPayload): string { })}` } -function resolveToolDisplayTitle(ui: unknown): string | undefined { - if (!isRecordLike(ui)) return undefined - return typeof ui.title === 'string' - ? ui.title - : typeof ui.phaseLabel === 'string' - ? ui.phaseLabel - : undefined -} - function appendTextBlock( blocks: RawPersistedBlock[], content: string, @@ -277,7 +269,6 @@ function buildLiveAssistantMessage(params: { case MothershipStreamV1EventType.tool: { const payload = parsed.payload const toolCallId = payload.toolCallId - const displayTitle = resolveToolDisplayTitle('ui' in payload ? payload.ui : undefined) if ('previewPhase' in payload) { continue @@ -312,7 +303,10 @@ function buildLiveAssistantMessage(params: { calledBy: scopedSubagent, ...(parentForBlock ? { parentToolCallId: parentForBlock } : {}), ...spanIdentity, - displayTitle, + displayTitle: getToolDisplayTitle( + payload.toolName, + isRecordLike(payload.arguments) ? payload.arguments : undefined + ), params: isRecordLike(payload.arguments) ? payload.arguments : undefined, state: typeof payload.status === 'string' ? payload.status : 'executing', }) diff --git a/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts b/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts index 91059cbbd2f..6081954a204 100644 --- a/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts +++ b/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts @@ -1160,9 +1160,6 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = { enum: ['call'], type: 'string', }, - requiresConfirmation: { - type: 'boolean', - }, status: { $ref: '#/$defs/MothershipStreamV1ToolStatus', }, @@ -1307,21 +1304,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = { hidden: { type: 'boolean', }, - icon: { - type: 'string', - }, internal: { type: 'boolean', }, - phaseLabel: { - type: 'string', - }, - requiresConfirmation: { - type: 'boolean', - }, - title: { - type: 'string', - }, }, type: 'object', }, diff --git a/apps/sim/lib/copilot/generated/mothership-stream-v1.ts b/apps/sim/lib/copilot/generated/mothership-stream-v1.ts index 32ca1d88d51..81e98257ddd 100644 --- a/apps/sim/lib/copilot/generated/mothership-stream-v1.ts +++ b/apps/sim/lib/copilot/generated/mothership-stream-v1.ts @@ -148,7 +148,6 @@ export interface MothershipStreamV1ToolCallDescriptor { mode: MothershipStreamV1ToolMode partial?: boolean phase: 'call' - requiresConfirmation?: boolean status?: MothershipStreamV1ToolStatus toolCallId: string toolName: string @@ -160,11 +159,7 @@ export interface MothershipStreamV1AdditionalPropertiesMap { export interface MothershipStreamV1ToolUI { clientExecutable?: boolean hidden?: boolean - icon?: string internal?: boolean - phaseLabel?: string - requiresConfirmation?: boolean - title?: string } export interface MothershipStreamV1ToolArgsDeltaEventEnvelope { payload: MothershipStreamV1ToolArgsDeltaPayload diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 0cd92c61c81..4a4c24a1594 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -14,12 +14,10 @@ export interface ToolCatalogEntry { | 'crawl_website' | 'create_file' | 'create_file_folder' - | 'create_folder' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_file' | 'delete_file_folder' - | 'delete_folder' | 'delete_workflow' | 'delete_workspace_mcp_server' | 'deploy' @@ -52,7 +50,6 @@ export interface ToolCatalogEntry { | 'knowledge' | 'knowledge_base' | 'list_file_folders' - | 'list_folders' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -60,6 +57,7 @@ export interface ToolCatalogEntry { | 'load_integration_tool' | 'manage_credential' | 'manage_custom_tool' + | 'manage_folder' | 'manage_mcp_tool' | 'manage_scheduled_task' | 'manage_skill' @@ -67,7 +65,6 @@ export interface ToolCatalogEntry { | 'media' | 'move_file' | 'move_file_folder' - | 'move_folder' | 'move_workflow' | 'oauth_get_auth_link' | 'oauth_request_access' @@ -115,12 +112,10 @@ export interface ToolCatalogEntry { | 'crawl_website' | 'create_file' | 'create_file_folder' - | 'create_folder' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_file' | 'delete_file_folder' - | 'delete_folder' | 'delete_workflow' | 'delete_workspace_mcp_server' | 'deploy' @@ -153,7 +148,6 @@ export interface ToolCatalogEntry { | 'knowledge' | 'knowledge_base' | 'list_file_folders' - | 'list_folders' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -161,6 +155,7 @@ export interface ToolCatalogEntry { | 'load_integration_tool' | 'manage_credential' | 'manage_custom_tool' + | 'manage_folder' | 'manage_mcp_tool' | 'manage_scheduled_task' | 'manage_skill' @@ -168,7 +163,6 @@ export interface ToolCatalogEntry { | 'media' | 'move_file' | 'move_file_folder' - | 'move_folder' | 'move_workflow' | 'oauth_get_auth_link' | 'oauth_request_access' @@ -208,7 +202,6 @@ export interface ToolCatalogEntry { | 'workspace_file' parameters: unknown requiredPermission?: 'admin' | 'read' | 'write' - requiresConfirmation?: boolean resultSchema?: unknown route: 'client' | 'go' | 'sim' | 'subagent' subagentId?: @@ -405,23 +398,6 @@ export const CreateFileFolder: ToolCatalogEntry = { requiredPermission: 'write', } -export const CreateFolder: ToolCatalogEntry = { - id: 'create_folder', - name: 'create_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - name: { type: 'string', description: 'Folder name.' }, - parentId: { type: 'string', description: 'Optional parent folder ID.' }, - workspaceId: { type: 'string', description: 'Optional workspace ID.' }, - }, - required: ['name'], - }, - requiredPermission: 'write', -} - export const CreateWorkflow: ToolCatalogEntry = { id: 'create_workflow', name: 'create_workflow', @@ -467,7 +443,6 @@ export const CreateWorkspaceMcpServer: ToolCatalogEntry = { }, required: ['name'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -515,27 +490,6 @@ export const DeleteFileFolder: ToolCatalogEntry = { }, required: ['paths'], }, - requiresConfirmation: true, - requiredPermission: 'write', -} - -export const DeleteFolder: ToolCatalogEntry = { - id: 'delete_folder', - name: 'delete_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - folderIds: { - type: 'array', - description: 'The folder IDs to delete.', - items: { type: 'string' }, - }, - }, - required: ['folderIds'], - }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -555,7 +509,6 @@ export const DeleteWorkflow: ToolCatalogEntry = { }, required: ['workflowIds'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -571,7 +524,6 @@ export const DeleteWorkspaceMcpServer: ToolCatalogEntry = { }, required: ['serverId'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -672,7 +624,6 @@ export const DeployApi: ToolCatalogEntry = { 'examples', ], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -818,7 +769,6 @@ export const DeployChat: ToolCatalogEntry = { 'examples', ], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -911,7 +861,6 @@ export const DeployMcp: ToolCatalogEntry = { }, required: ['deploymentType', 'deploymentStatus'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -1465,7 +1414,6 @@ export const GenerateApiKey: ToolCatalogEntry = { }, required: ['name'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -2346,7 +2294,6 @@ export const KnowledgeBase: ToolCatalogEntry = { }, required: ['success', 'message'], }, - requiresConfirmation: true, } export const ListFileFolders: ToolCatalogEntry = { @@ -2366,19 +2313,6 @@ export const ListFileFolders: ToolCatalogEntry = { requiredPermission: 'read', } -export const ListFolders: ToolCatalogEntry = { - id: 'list_folders', - name: 'list_folders', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - workspaceId: { type: 'string', description: 'Optional workspace ID to list folders for.' }, - }, - }, -} - export const ListIntegrationTools: ToolCatalogEntry = { id: 'list_integration_tools', name: 'list_integration_tools', @@ -2442,7 +2376,6 @@ export const LoadDeployment: ToolCatalogEntry = { }, required: ['version'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -2488,7 +2421,6 @@ export const ManageCredential: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -2558,7 +2490,50 @@ export const ManageCustomTool: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, + requiredPermission: 'write', +} + +export const ManageFolder: ToolCatalogEntry = { + id: 'manage_folder', + name: 'manage_folder', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + destinationPath: { + type: 'string', + description: + 'Destination parent folder\'s VFS path for move/create. Omit (or pass "workflows") to target the workspace root.', + }, + folderId: { + type: 'string', + description: + 'Target folder ID, used as a fallback when path is not given. Readable from a contained workflow\'s meta.json "folderId".', + }, + name: { + type: 'string', + description: + 'Folder name. Required for rename (the new name); for create when you pass a destination parent instead of a full path.', + }, + operation: { + type: 'string', + description: 'The operation to perform.', + enum: ['create', 'rename', 'move', 'delete'], + }, + parentId: { + type: 'string', + description: + 'Destination parent folder ID, used as a fallback when destinationPath is not given.', + }, + path: { + type: 'string', + description: + 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path. Identifies the folder for rename/move/delete; for create it is the new folder\'s full path (its parent must already exist).', + }, + }, + required: ['operation'], + }, requiredPermission: 'write', } @@ -2610,7 +2585,6 @@ export const ManageMcpTool: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -2723,7 +2697,6 @@ export const ManageSkill: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -2820,26 +2793,6 @@ export const MoveFileFolder: ToolCatalogEntry = { requiredPermission: 'write', } -export const MoveFolder: ToolCatalogEntry = { - id: 'move_folder', - name: 'move_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - folderId: { type: 'string', description: 'The folder ID to move.' }, - parentId: { - type: 'string', - description: - 'Target parent folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['folderId'], - }, - requiredPermission: 'write', -} - export const MoveWorkflow: ToolCatalogEntry = { id: 'move_workflow', name: 'move_workflow', @@ -2897,7 +2850,6 @@ export const OauthRequestAccess: ToolCatalogEntry = { }, required: ['providerName'], }, - requiresConfirmation: true, } export const OpenResource: ToolCatalogEntry = { @@ -2955,7 +2907,6 @@ export const PromoteToLive: ToolCatalogEntry = { }, required: ['version'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -3161,7 +3112,6 @@ export const Redeploy: ToolCatalogEntry = { 'examples', ], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -3286,7 +3236,6 @@ export const RestoreResource: ToolCatalogEntry = { }, required: ['type', 'id'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -3342,7 +3291,6 @@ export const RunBlock: ToolCatalogEntry = { required: ['blockId'], }, clientExecutable: true, - requiresConfirmation: true, } export const RunFromBlock: ToolCatalogEntry = { @@ -3377,7 +3325,6 @@ export const RunFromBlock: ToolCatalogEntry = { required: ['startBlockId'], }, clientExecutable: true, - requiresConfirmation: true, } export const RunWorkflow: ToolCatalogEntry = { @@ -3421,7 +3368,6 @@ export const RunWorkflow: ToolCatalogEntry = { }, }, clientExecutable: true, - requiresConfirmation: true, } export const RunWorkflowUntilBlock: ToolCatalogEntry = { @@ -3470,7 +3416,6 @@ export const RunWorkflowUntilBlock: ToolCatalogEntry = { required: ['stopAfterBlockId'], }, clientExecutable: true, - requiresConfirmation: true, } export const ScheduledTask: ToolCatalogEntry = { @@ -3666,7 +3611,6 @@ export const SetEnvironmentVariables: ToolCatalogEntry = { }, required: ['variables'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -3706,7 +3650,6 @@ export const SetGlobalWorkflowVariables: ToolCatalogEntry = { }, required: ['operations'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -3772,7 +3715,6 @@ export const UpdateDeploymentVersion: ToolCatalogEntry = { }, required: ['version'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -3810,7 +3752,6 @@ export const UpdateWorkspaceMcpServer: ToolCatalogEntry = { }, required: ['serverId'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -4189,7 +4130,6 @@ export const UserTable: ToolCatalogEntry = { }, required: ['success', 'message'], }, - requiresConfirmation: true, } export const Workflow: ToolCatalogEntry = { @@ -4439,6 +4379,23 @@ export const ManageCustomToolOperationValues = [ ManageCustomToolOperation.list, ] as const +export const ManageFolderOperation = { + create: 'create', + rename: 'rename', + move: 'move', + delete: 'delete', +} as const + +export type ManageFolderOperation = + (typeof ManageFolderOperation)[keyof typeof ManageFolderOperation] + +export const ManageFolderOperationValues = [ + ManageFolderOperation.create, + ManageFolderOperation.rename, + ManageFolderOperation.move, + ManageFolderOperation.delete, +] as const + export const ManageMcpToolOperation = { add: 'add', edit: 'edit', @@ -4615,12 +4572,10 @@ export const TOOL_CATALOG: Record = { [CrawlWebsite.id]: CrawlWebsite, [CreateFile.id]: CreateFile, [CreateFileFolder.id]: CreateFileFolder, - [CreateFolder.id]: CreateFolder, [CreateWorkflow.id]: CreateWorkflow, [CreateWorkspaceMcpServer.id]: CreateWorkspaceMcpServer, [DeleteFile.id]: DeleteFile, [DeleteFileFolder.id]: DeleteFileFolder, - [DeleteFolder.id]: DeleteFolder, [DeleteWorkflow.id]: DeleteWorkflow, [DeleteWorkspaceMcpServer.id]: DeleteWorkspaceMcpServer, [Deploy.id]: Deploy, @@ -4653,7 +4608,6 @@ export const TOOL_CATALOG: Record = { [Knowledge.id]: Knowledge, [KnowledgeBase.id]: KnowledgeBase, [ListFileFolders.id]: ListFileFolders, - [ListFolders.id]: ListFolders, [ListIntegrationTools.id]: ListIntegrationTools, [ListUserWorkspaces.id]: ListUserWorkspaces, [ListWorkspaceMcpServers.id]: ListWorkspaceMcpServers, @@ -4661,6 +4615,7 @@ export const TOOL_CATALOG: Record = { [LoadIntegrationTool.id]: LoadIntegrationTool, [ManageCredential.id]: ManageCredential, [ManageCustomTool.id]: ManageCustomTool, + [ManageFolder.id]: ManageFolder, [ManageMcpTool.id]: ManageMcpTool, [ManageScheduledTask.id]: ManageScheduledTask, [ManageSkill.id]: ManageSkill, @@ -4668,7 +4623,6 @@ export const TOOL_CATALOG: Record = { [Media.id]: Media, [MoveFile.id]: MoveFile, [MoveFileFolder.id]: MoveFileFolder, - [MoveFolder.id]: MoveFolder, [MoveWorkflow.id]: MoveWorkflow, [OauthGetAuthLink.id]: OauthGetAuthLink, [OauthRequestAccess.id]: OauthRequestAccess, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 31171237e7e..dcaea0db6ea 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -180,27 +180,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - create_folder: { - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Folder name.', - }, - parentId: { - type: 'string', - description: 'Optional parent folder ID.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID.', - }, - }, - required: ['name'], - }, - resultSchema: undefined, - }, create_workflow: { parameters: { type: 'object', @@ -305,22 +284,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - delete_folder: { - parameters: { - type: 'object', - properties: { - folderIds: { - type: 'array', - description: 'The folder IDs to delete.', - items: { - type: 'string', - }, - }, - }, - required: ['folderIds'], - }, - resultSchema: undefined, - }, delete_workflow: { parameters: { type: 'object', @@ -2154,18 +2117,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - list_folders: { - parameters: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Optional workspace ID to list folders for.', - }, - }, - }, - resultSchema: undefined, - }, list_integration_tools: { parameters: { properties: { @@ -2345,6 +2296,45 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + manage_folder: { + parameters: { + type: 'object', + properties: { + destinationPath: { + type: 'string', + description: + 'Destination parent folder\'s VFS path for move/create. Omit (or pass "workflows") to target the workspace root.', + }, + folderId: { + type: 'string', + description: + 'Target folder ID, used as a fallback when path is not given. Readable from a contained workflow\'s meta.json "folderId".', + }, + name: { + type: 'string', + description: + 'Folder name. Required for rename (the new name); for create when you pass a destination parent instead of a full path.', + }, + operation: { + type: 'string', + description: 'The operation to perform.', + enum: ['create', 'rename', 'move', 'delete'], + }, + parentId: { + type: 'string', + description: + 'Destination parent folder ID, used as a fallback when destinationPath is not given.', + }, + path: { + type: 'string', + description: + 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path. Identifies the folder for rename/move/delete; for create it is the new folder\'s full path (its parent must already exist).', + }, + }, + required: ['operation'], + }, + resultSchema: undefined, + }, manage_mcp_tool: { parameters: { type: 'object', @@ -2581,24 +2571,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - move_folder: { - parameters: { - type: 'object', - properties: { - folderId: { - type: 'string', - description: 'The folder ID to move.', - }, - parentId: { - type: 'string', - description: - 'Target parent folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['folderId'], - }, - resultSchema: undefined, - }, move_workflow: { parameters: { type: 'object', diff --git a/apps/sim/lib/copilot/request/handlers/handlers.test.ts b/apps/sim/lib/copilot/request/handlers/handlers.test.ts index 8d4cb83c669..7d341238c4e 100644 --- a/apps/sim/lib/copilot/request/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/request/handlers/handlers.test.ts @@ -168,10 +168,7 @@ describe('sse-handlers tool lifecycle', () => { executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.call, - ui: { - title: 'Reading foo.txt', - phaseLabel: 'Workspace', - }, + ui: {}, }, } satisfies StreamEvent, context, @@ -197,53 +194,16 @@ describe('sse-handlers tool lifecycle', () => { const updated = context.toolCalls.get('tool-1') expect(updated?.status).toBe(MothershipStreamV1ToolOutcome.success) - expect(updated?.displayTitle).toBe('Reading foo.txt') + // Display titles are derived client-side from the tool name (+args), not the + // stream; read with no path resolves to the static "Reading file". + expect(updated?.displayTitle).toBe('Reading file') expect(updated?.result?.output).toEqual({ ok: true }) expect(context.contentBlocks.at(0)).toEqual( expect.objectContaining({ type: 'tool_call', toolCall: expect.objectContaining({ id: 'tool-1', - displayTitle: 'Reading foo.txt', - }), - }) - ) - }) - - it('uses phaseLabel as a display title fallback when no title is provided', async () => { - executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } }) - const onEvent = vi.fn() - - await sseHandlers.tool( - { - type: MothershipStreamV1EventType.tool, - payload: { - toolCallId: 'tool-phase-label', - toolName: ReadTool.id, - arguments: { workflowId: 'workflow-1' }, - executor: MothershipStreamV1ToolExecutor.sim, - mode: MothershipStreamV1ToolMode.async, - phase: MothershipStreamV1ToolPhase.call, - ui: { - phaseLabel: 'Workspace', - }, - }, - } satisfies StreamEvent, - context, - execContext, - { onEvent, interactive: false, timeout: 1000 } - ) - - await sleep(0) - - const updated = context.toolCalls.get('tool-phase-label') - expect(updated?.displayTitle).toBe('Workspace') - expect(context.contentBlocks.at(0)).toEqual( - expect.objectContaining({ - type: 'tool_call', - toolCall: expect.objectContaining({ - id: 'tool-phase-label', - displayTitle: 'Workspace', + displayTitle: 'Reading file', }), }) ) diff --git a/apps/sim/lib/copilot/request/handlers/tool.ts b/apps/sim/lib/copilot/request/handlers/tool.ts index 7e76a57b2f3..be028b2b1fc 100644 --- a/apps/sim/lib/copilot/request/handlers/tool.ts +++ b/apps/sim/lib/copilot/request/handlers/tool.ts @@ -30,6 +30,7 @@ import type { } from '@/lib/copilot/request/types' import { getToolEntry, isSimExecuted } from '@/lib/copilot/tool-executor' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' +import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display' import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools' import type { ToolScope } from './types' import { @@ -50,13 +51,12 @@ import { const logger = createLogger('CopilotToolHandler') -function applyToolDisplay( - toolCall: ToolCallState | undefined, - ui: { title?: string; phaseLabel?: string; hidden?: boolean } -): void { - if (!toolCall) return - const displayTitle = ui.title || ui.phaseLabel - if (displayTitle) toolCall.displayTitle = displayTitle +function applyToolDisplay(toolCall: ToolCallState | undefined): void { + if (!toolCall?.name) return + toolCall.displayTitle = getToolDisplayTitle( + toolCall.name, + toolCall.params as Record | undefined + ) } /** @@ -262,7 +262,7 @@ async function handleCallPhase( if (wasToolResultSeen(toolCallId) || existing?.endTime) { if (existing && !existing.name && toolName) existing.name = toolName if (existing && !existing.params && args) existing.params = args - applyToolDisplay(existing, ui) + applyToolDisplay(existing) return } } else { @@ -272,7 +272,7 @@ async function handleCallPhase( ) { if (!existing.name && toolName) existing.name = toolName if (!existing.params && args) existing.params = args - applyToolDisplay(existing, ui) + applyToolDisplay(existing) return } } @@ -377,7 +377,7 @@ function registerSubagentToolCall( if (toolCall) { if (!toolCall.name && toolName) toolCall.name = toolName if (args && !toolCall.params) toolCall.params = args - applyToolDisplay(toolCall, ui) + applyToolDisplay(toolCall) if (hideFromUi) removeToolCallContentBlock(context, toolCallId) } else { toolCall = { @@ -387,7 +387,7 @@ function registerSubagentToolCall( params: args, startTime: Date.now(), } - applyToolDisplay(toolCall, ui) + applyToolDisplay(toolCall) context.toolCalls.set(toolCallId, toolCall) const parentToolCall = context.toolCalls.get(parentToolCallId) if (!hideFromUi) { @@ -406,7 +406,7 @@ function registerSubagentToolCall( if (existingSubagentToolCall) { if (!existingSubagentToolCall.name && toolName) existingSubagentToolCall.name = toolName if (args && !existingSubagentToolCall.params) existingSubagentToolCall.params = args - applyToolDisplay(existingSubagentToolCall, ui) + applyToolDisplay(existingSubagentToolCall) } else { subagentToolCalls.push(toolCall) } @@ -423,7 +423,7 @@ function registerMainToolCall( const hideFromUi = isToolHiddenInUi(toolName) || ui.hidden === true if (existing) { if (args && !existing.params) existing.params = args - applyToolDisplay(existing, ui) + applyToolDisplay(existing) if (hideFromUi) { removeToolCallContentBlock(context, toolCallId) return @@ -442,7 +442,7 @@ function registerMainToolCall( params: args, startTime: Date.now(), } - applyToolDisplay(created, ui) + applyToolDisplay(created) context.toolCalls.set(toolCallId, created) if (!hideFromUi) { addContentBlock(context, { type: 'tool_call', toolCall: created }) diff --git a/apps/sim/lib/copilot/request/handlers/types.ts b/apps/sim/lib/copilot/request/handlers/types.ts index 21543ce5d60..e35c1ba5efc 100644 --- a/apps/sim/lib/copilot/request/handlers/types.ts +++ b/apps/sim/lib/copilot/request/handlers/types.ts @@ -167,28 +167,23 @@ export function abortPendingToolIfStreamDead( } /** - * Extract the `ui` object from a typed tool_call payload. The Go backend enriches - * tool_call events with `ui: { requiresConfirmation, clientExecutable, ... }`. + * Extract the behavioral `ui` flags from a typed tool_call payload. The Go + * backend enriches tool_call events with `ui: { clientExecutable, internal, + * hidden }`; presentation (title/icon) is derived client-side from the tool name. */ export function getToolCallUI(data: MothershipStreamV1ToolCallDescriptor): { - requiresConfirmation: boolean clientExecutable: boolean simExecutable: boolean internal: boolean hidden: boolean - title?: string - phaseLabel?: string } { const raw = asRecord(data.ui) return { - requiresConfirmation: raw.requiresConfirmation === true || data.requiresConfirmation === true, clientExecutable: raw.clientExecutable === true || data.executor === MothershipStreamV1ToolExecutor.client, simExecutable: data.executor === MothershipStreamV1ToolExecutor.sim, internal: raw.internal === true, hidden: raw.hidden === true, - title: typeof raw.title === 'string' ? raw.title : undefined, - phaseLabel: typeof raw.phaseLabel === 'string' ? raw.phaseLabel : undefined, } } diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index 48784f1a6bb..b39027e3526 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -2,10 +2,8 @@ import { createLogger } from '@sim/logger' import { CheckDeploymentStatus, CompleteScheduledTask, - CreateFolder, CreateWorkflow, CreateWorkspaceMcpServer, - DeleteFolder, DeleteWorkflow, DeleteWorkspaceMcpServer, DeployApi, @@ -23,18 +21,17 @@ import { GetWorkflowRunOptions, Glob as GlobTool, Grep as GrepTool, - ListFolders, ListIntegrationTools, ListUserWorkspaces, ListWorkspaceMcpServers, LoadDeployment, ManageCredential, ManageCustomTool, + ManageFolder, ManageMcpTool, ManageScheduledTask, ManageSkill, MaterializeFile, - MoveFolder, MoveWorkflow, OauthGetAuthLink, OauthRequestAccess, @@ -92,12 +89,10 @@ import { executeOpenResource } from '../tools/handlers/resources' import { executeRestoreResource } from '../tools/handlers/restore-resource' import { executeVfsGlob, executeVfsGrep, executeVfsRead } from '../tools/handlers/vfs' import { - executeCreateFolder, executeCreateWorkflow, - executeDeleteFolder, executeDeleteWorkflow, executeGenerateApiKey, - executeMoveFolder, + executeManageFolder, executeMoveWorkflow, executeRenameWorkflow, executeRunBlock, @@ -113,7 +108,6 @@ import { executeGetDeployedWorkflowState, executeGetWorkflowData, executeGetWorkflowRunOptions, - executeListFolders, executeListUserWorkspaces, } from '../tools/handlers/workflow/queries' import { registerHandlers } from './executor' @@ -140,7 +134,6 @@ function h(fn: (params: any, context: any) => Promise): ToolHandler { function buildHandlerMap(): Record { return { [ListUserWorkspaces.id]: h((_p, c) => executeListUserWorkspaces(c)), - [ListFolders.id]: h(executeListFolders), [GetWorkflowData.id]: h(executeGetWorkflowData), [GetWorkflowRunOptions.id]: h(executeGetWorkflowRunOptions), [GetBlockOutputs.id]: h(executeGetBlockOutputs), @@ -148,12 +141,10 @@ function buildHandlerMap(): Record { [GetDeployedWorkflowState.id]: h(executeGetDeployedWorkflowState), [CreateWorkflow.id]: h(executeCreateWorkflow), - [CreateFolder.id]: h(executeCreateFolder), [DeleteWorkflow.id]: h(executeDeleteWorkflow), - [DeleteFolder.id]: h(executeDeleteFolder), [RenameWorkflow.id]: h(executeRenameWorkflow), [MoveWorkflow.id]: h(executeMoveWorkflow), - [MoveFolder.id]: h(executeMoveFolder), + [ManageFolder.id]: h(executeManageFolder), [RunWorkflow.id]: h(executeRunWorkflow), [RunWorkflowUntilBlock.id]: h(executeRunWorkflowUntilBlock), [RunFromBlock.id]: h(executeRunFromBlock), diff --git a/apps/sim/lib/copilot/tools/handlers/param-types.ts b/apps/sim/lib/copilot/tools/handlers/param-types.ts index b0c28e25b7a..da437b4993a 100644 --- a/apps/sim/lib/copilot/tools/handlers/param-types.ts +++ b/apps/sim/lib/copilot/tools/handlers/param-types.ts @@ -27,10 +27,6 @@ export interface GetBlockUpstreamReferencesParams { blockIds: string[] } -export interface ListFoldersParams { - workspaceId?: string -} - // === Workflow Mutation Params === export interface CreateWorkflowParams { @@ -262,6 +258,15 @@ export interface DeleteFolderParams { folderIds: string[] } +export interface ManageFolderParams { + operation: string + path?: string + folderId?: string + name?: string + destinationPath?: string + parentId?: string | null +} + export interface UpdateWorkspaceMcpServerParams { serverId: string name?: string diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 7ae8c2e6a4d..e72abc1082d 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -8,6 +8,11 @@ import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblock import { eq } from 'drizzle-orm' import { performCreateWorkspaceApiKey } from '@/lib/api-key/orchestration' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { + buildVfsFolderPathMap, + decodeVfsPathSegments, + encodeVfsPathSegments, +} from '@/lib/copilot/vfs/path-utils' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' import { getSocketServerUrl } from '@/lib/core/utils/urls' @@ -307,6 +312,7 @@ import type { DeleteFolderParams, DeleteWorkflowParams, GenerateApiKeyParams, + ManageFolderParams, MoveFolderParams, MoveWorkflowParams, RenameFolderParams, @@ -1242,6 +1248,152 @@ async function executeRenameFolder( } } +/** + * Strip the `workflows/` VFS prefix from a folder path, returning the + * folder-relative remainder. `workflows` (or an empty path) maps to the + * workspace root and yields an empty string. + */ +function workflowFolderRelativePath(rawPath: string): string { + const trimmed = rawPath.trim().replace(/^\/+|\/+$/g, '') + if (!trimmed || trimmed === 'workflows') return '' + return trimmed.startsWith('workflows/') ? trimmed.slice('workflows/'.length) : trimmed +} + +/** + * Load a lookup from each folder's canonical encoded VFS path to its id by + * inverting the same {@link buildVfsFolderPathMap} the VFS uses to serve folder + * paths, so a path the agent sees via glob round-trips to the right id. Fetched + * once per manage_folder call and reused across target + parent resolution. + */ +async function loadFolderPathToIdMap(workspaceId: string): Promise> { + const byPath = new Map() + for (const [folderId, encodedPath] of buildVfsFolderPathMap( + await listFolders(workspaceId) + ).entries()) { + byPath.set(encodedPath, folderId) + } + return byPath +} + +function lookupFolderIdByPath(rawPath: string, byPath: Map): string | null { + const relative = workflowFolderRelativePath(rawPath) + if (!relative) return null + return byPath.get(encodeVfsPathSegments(decodeVfsPathSegments(relative))) ?? null +} + +/** Resolve the folder a manage_folder op targets, preferring folderId over path. */ +async function resolveManageFolderTarget( + params: ManageFolderParams, + getFolderPaths: () => Promise> +): Promise<{ folderId: string } | { error: string }> { + const directId = typeof params.folderId === 'string' ? params.folderId.trim() : '' + if (directId) return { folderId: directId } + const path = typeof params.path === 'string' ? params.path.trim() : '' + if (!path) return { error: 'Provide the folder path (e.g. "workflows/Marketing") or folderId.' } + const folderId = lookupFolderIdByPath(path, await getFolderPaths()) + if (!folderId) return { error: `Folder not found at ${path}` } + return { folderId } +} + +/** + * Resolve the destination parent for move/create. parentId/destinationPath are + * optional; their absence (or an explicit root) targets the workspace root + * (parentId null). + */ +async function resolveManageFolderParent( + params: ManageFolderParams, + getFolderPaths: () => Promise> +): Promise<{ parentId: string | null } | { error: string }> { + const directId = typeof params.parentId === 'string' ? params.parentId.trim() : '' + if (directId) return { parentId: directId } + if (params.parentId === null) return { parentId: null } + const dest = typeof params.destinationPath === 'string' ? params.destinationPath.trim() : '' + if (!dest || !workflowFolderRelativePath(dest)) return { parentId: null } + const parentId = lookupFolderIdByPath(dest, await getFolderPaths()) + if (!parentId) return { error: `Destination folder not found at ${dest}` } + return { parentId } +} + +/** + * Single entry point for folder CRUD (create/rename/move/delete). Resolves the + * VFS-path/folderId handles, then delegates to the existing folder handlers so + * all DB orchestration (performCreateFolder / performUpdateFolder / + * performDeleteFolder) stays in one place. + */ +export async function executeManageFolder( + params: ManageFolderParams, + context: ExecutionContext +): Promise { + try { + const operation = typeof params?.operation === 'string' ? params.operation.trim() : '' + const workspaceId = context.workspaceId || (await getDefaultWorkspaceId(context.userId)) + + // Fetch the workspace folder list at most once, lazily — only when a path + // (vs an explicit id) actually needs resolving, and shared across the + // target + parent lookups a single move/create performs. + let folderPathsPromise: Promise> | undefined + const getFolderPaths = () => (folderPathsPromise ??= loadFolderPathToIdMap(workspaceId)) + + switch (operation) { + case 'create': { + let name = typeof params.name === 'string' ? params.name.trim() : '' + let parentId: string | null = null + const path = typeof params.path === 'string' ? params.path.trim() : '' + if (!name && path) { + const segments = decodeVfsPathSegments(workflowFolderRelativePath(path)) + if (segments.length === 0) { + return { success: false, error: 'create requires a folder name or path' } + } + name = segments[segments.length - 1] + const parentSegments = segments.slice(0, -1) + if (parentSegments.length > 0) { + const resolved = lookupFolderIdByPath( + encodeVfsPathSegments(parentSegments), + await getFolderPaths() + ) + if (!resolved) { + return { success: false, error: `Parent folder not found for ${path}` } + } + parentId = resolved + } + } else { + const parent = await resolveManageFolderParent(params, getFolderPaths) + if ('error' in parent) return { success: false, error: parent.error } + parentId = parent.parentId + } + if (!name) return { success: false, error: 'create requires a folder name or path' } + return executeCreateFolder({ name, parentId: parentId ?? undefined, workspaceId }, context) + } + case 'rename': { + const name = typeof params.name === 'string' ? params.name.trim() : '' + if (!name) return { success: false, error: 'rename requires a new name' } + const target = await resolveManageFolderTarget(params, getFolderPaths) + if ('error' in target) return { success: false, error: target.error } + return executeRenameFolder({ folderId: target.folderId, name }, context) + } + case 'move': { + const target = await resolveManageFolderTarget(params, getFolderPaths) + if ('error' in target) return { success: false, error: target.error } + const parent = await resolveManageFolderParent(params, getFolderPaths) + if ('error' in parent) return { success: false, error: parent.error } + return executeMoveFolder({ folderId: target.folderId, parentId: parent.parentId }, context) + } + case 'delete': { + const target = await resolveManageFolderTarget(params, getFolderPaths) + if ('error' in target) return { success: false, error: target.error } + return executeDeleteFolder({ folderIds: [target.folderId] }, context) + } + default: + return { + success: false, + error: `Unknown operation "${operation}". Use create, rename, move, or delete.`, + } + } + } catch (error) { + return { success: false, error: toError(error).message } + } +} + export async function executeRunBlock( params: RunBlockParams, context: ExecutionContext diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts b/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts index 41ea582105f..f7b556ca453 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts @@ -14,19 +14,18 @@ import { } from '@/lib/workflows/persistence/utils' import { resolveTriggerRunOptions, toPublicRunOption } from '@/lib/workflows/triggers/run-options' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' -import { getWorkflowById, listFolders } from '@/lib/workflows/utils' +import { getWorkflowById } from '@/lib/workflows/utils' import { listUserWorkspaces } from '@/lib/workspaces/utils' import { getBlock } from '@/blocks/registry' import { normalizeName } from '@/executor/constants' import type { Loop, Parallel } from '@/stores/workflows/workflow/types' -import { ensureWorkflowAccess, ensureWorkspaceAccess, getDefaultWorkspaceId } from '../access' +import { ensureWorkflowAccess } from '../access' import type { GetBlockOutputsParams, GetBlockUpstreamReferencesParams, GetDeployedWorkflowStateParams, GetWorkflowDataParams, GetWorkflowRunOptionsParams, - ListFoldersParams, } from '../param-types' export async function executeListUserWorkspaces( @@ -41,32 +40,6 @@ export async function executeListUserWorkspaces( } } -export async function executeListFolders( - params: ListFoldersParams, - context: ExecutionContext -): Promise { - try { - const workspaceId = - (params?.workspaceId as string | undefined) || - context.workspaceId || - (await getDefaultWorkspaceId(context.userId)) - - await ensureWorkspaceAccess(workspaceId, context.userId, 'read') - - const folders = await listFolders(workspaceId) - - return { - success: true, - output: { - workspaceId, - folders, - }, - } - } catch (error) { - return { success: false, error: toError(error).message } - } -} - export async function executeGetWorkflowRunOptions( params: GetWorkflowRunOptionsParams, context: ExecutionContext diff --git a/apps/sim/lib/copilot/tools/mcp/definitions.ts b/apps/sim/lib/copilot/tools/mcp/definitions.ts deleted file mode 100644 index 8adede9671b..00000000000 --- a/apps/sim/lib/copilot/tools/mcp/definitions.ts +++ /dev/null @@ -1,535 +0,0 @@ -import type { Tool } from '@modelcontextprotocol/sdk/types.js' - -export type ToolAnnotations = NonNullable - -export type DirectToolDef = { - name: string - description: string - inputSchema: Tool['inputSchema'] - toolId: string - annotations?: ToolAnnotations -} - -export type SubagentToolDef = { - name: string - description: string - inputSchema: Tool['inputSchema'] - agentId: string - annotations?: ToolAnnotations -} - -/** - * Direct tools that execute immediately without LLM orchestration. - * These are fast database queries that don't need AI reasoning. - */ -export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ - { - name: 'list_workspaces', - toolId: 'list_user_workspaces', - description: - 'List all workspaces the user has access to. Returns workspace IDs, names, and roles. Use this first to determine which workspace to operate in.', - inputSchema: { - type: 'object', - properties: {}, - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'list_folders', - toolId: 'list_folders', - description: - 'List all folders in a workspace. Returns folder IDs, names, and parent relationships for organizing workflows.', - inputSchema: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Workspace ID to list folders from.', - }, - }, - required: ['workspaceId'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'create_workflow', - toolId: 'create_workflow', - description: - 'Create a new empty workflow. Returns the new workflow ID. Always call this FIRST before sim_workflow for new workflows. Use workspaceId to place it in a specific workspace.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Name for the new workflow.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Uses default workspace if not provided.', - }, - folderId: { - type: 'string', - description: 'Optional folder ID to place the workflow in.', - }, - description: { - type: 'string', - description: 'Optional description for the workflow.', - }, - }, - required: ['name'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'create_folder', - toolId: 'create_folder', - description: - 'Create a new folder for organizing workflows. Use parentId to create nested folder hierarchies.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Name for the new folder.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Uses default workspace if not provided.', - }, - parentId: { - type: 'string', - description: 'Optional parent folder ID for nested folders.', - }, - }, - required: ['name'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'rename_workflow', - toolId: 'rename_workflow', - description: 'Rename an existing workflow.', - inputSchema: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'The workflow ID to rename.', - }, - name: { - type: 'string', - description: 'The new name for the workflow.', - }, - }, - required: ['workflowId', 'name'], - }, - annotations: { destructiveHint: false, idempotentHint: true }, - }, - { - name: 'move_workflow', - toolId: 'move_workflow', - description: - 'Move a workflow into a different folder. Omit folderId or pass empty string to move to workspace root.', - inputSchema: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'The workflow ID to move.', - }, - folderId: { - type: 'string', - description: 'Target folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['workflowId'], - }, - annotations: { destructiveHint: false, idempotentHint: true }, - }, - { - name: 'move_folder', - toolId: 'move_folder', - description: - 'Move a folder into another folder. Omit parentId or pass empty string to move to workspace root.', - inputSchema: { - type: 'object', - properties: { - folderId: { - type: 'string', - description: 'The folder ID to move.', - }, - parentId: { - type: 'string', - description: - 'Target parent folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['folderId'], - }, - annotations: { destructiveHint: false, idempotentHint: true }, - }, - { - name: 'get_deployed_workflow_state', - toolId: 'get_deployed_workflow_state', - description: - 'Get the deployed (production) state of a workflow. Returns the full workflow definition as deployed, or indicates if the workflow is not yet deployed.', - inputSchema: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to get the deployed state for.', - }, - }, - required: ['workflowId'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'generate_api_key', - toolId: 'generate_api_key', - description: - 'Generate a new workspace API key for calling workflow API endpoints. The key is only shown once — tell the user to save it immediately.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: - 'A descriptive name for the API key (e.g., "production-key", "dev-testing").', - }, - workspaceId: { - type: 'string', - description: "Optional workspace ID. Defaults to user's default workspace.", - }, - }, - required: ['name'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'create_job', - toolId: 'create_job', - description: - 'Create a scheduled background job that runs a prompt against Sim at a specified frequency or time. Use for polling, reminders, or deferred tasks. Provide cron for recurring jobs or time for one-time execution.', - inputSchema: { - type: 'object', - properties: { - title: { - type: 'string', - description: 'A short descriptive title for the job (e.g., "Email Poller").', - }, - prompt: { - type: 'string', - description: 'The prompt to execute when the job fires.', - }, - cron: { - type: 'string', - description: - 'Cron expression for recurring jobs (e.g., "*/5 * * * *" for every 5 minutes).', - }, - time: { - type: 'string', - description: - 'ISO 8601 datetime for one-time jobs or cron start time (e.g., "2026-03-06T09:00:00").', - }, - timezone: { - type: 'string', - description: 'IANA timezone (default: UTC).', - }, - lifecycle: { - type: 'string', - description: - '"persistent" (default, runs indefinitely) or "until_complete" (runs until complete_scheduled_task is called).', - }, - successCondition: { - type: 'string', - description: - 'What must happen for the job to be considered complete. Used with until_complete lifecycle.', - }, - maxRuns: { - type: 'number', - description: 'Maximum number of executions before the job auto-completes. Safety limit.', - }, - }, - required: ['title', 'prompt'], - }, - annotations: { destructiveHint: false }, - }, -] - -export const SUBAGENT_TOOL_DEFS: SubagentToolDef[] = [ - { - name: 'sim_workflow', - agentId: 'workflow', - description: `Create, modify, test, debug, and organize workflows end-to-end in a single step. - -USE THIS WHEN: -- Building a new workflow from scratch -- Modifying an existing workflow -- You want to gather information and build in one pass -- Moving, renaming, or organizing workflows and folders - -WORKFLOW ID (REQUIRED): -- For NEW workflows: First call create_workflow to get a workflowId, then pass it here -- For EXISTING workflows: Always pass the workflowId parameter - -CAN DO: -- Gather information about blocks, credentials, patterns -- Search documentation and patterns for best practices -- Add, modify, or remove blocks -- Configure block settings and connections -- Set environment variables and workflow variables -- Move, rename, delete workflows and folders -- Run or inspect workflows through the nested run/debug specialists when validation is needed -- Delegate deployment or auth setup to the nested specialists when needed - -CANNOT DO: -- Replace dedicated testing flows like sim_test when you want a standalone execution-only pass -- Replace dedicated deploy flows like sim_deploy when you want deployment as a separate step - -WORKFLOW: -1. Call create_workflow to get a workflowId (for new workflows) -2. Call sim_workflow with the request and workflowId -3. Workflow agent gathers info, builds, and can delegate run/debug/auth/deploy help in one pass -4. Call sim_test when you want a dedicated execution-only verification pass -5. Optionally call sim_deploy to make it externally accessible`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'What you want to build, modify, or organize.', - }, - workflowId: { - type: 'string', - description: - 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_discovery', - agentId: 'discovery', - description: `Find workflows by their contents or functionality when the user doesn't know the exact name or ID. - -USE THIS WHEN: -- User describes a workflow by what it does: "the one that sends emails", "my Slack notification workflow" -- User refers to workflow contents: "the workflow with the OpenAI block" -- User needs to search/match workflows by functionality or description - -DO NOT USE (use direct tools instead): -- User knows the workflow name → use get_workflow -- User wants to list all workflows → use list_workflows -- User wants to list workspaces → use list_workspaces -- User wants to list folders → use list_folders`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workspaceId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'sim_deploy', - agentId: 'deploy', - description: `Deploy a workflow to make it accessible externally. Workflows can be tested without deploying, but deployment is needed for API access, chat UIs, or MCP exposure. - -DEPLOYMENT TYPES: -- "deploy as api" - REST API endpoint for programmatic access -- "deploy as chat" - Managed chat UI with auth options -- "deploy as mcp" - Expose as MCP tool on an MCP server for AI agents to call - -MCP DEPLOYMENT FLOW: -The deploy subagent will automatically: list available MCP servers → create one if needed → deploy the workflow as an MCP tool to that server. You can specify server name, tool name, and tool description. - -ALSO CAN: -- Get the deployed (production) state to compare with draft -- Generate workspace API keys for calling deployed workflows -- List and create MCP servers in the workspace`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'The deployment request, e.g. "deploy as api" or "deploy as chat"', - }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to deploy.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_test', - agentId: 'run', - description: `Run a workflow and verify its outputs. Works on both deployed and undeployed (draft) workflows. Use after building to verify correctness. - -Supports full and partial execution: -- Full run with test inputs -- Stop after a specific block (run_workflow_until_block) -- Run a single block in isolation (run_block) -- Resume from a specific block (run_from_block)`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to test.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_auth', - agentId: 'auth', - description: - 'Check OAuth connection status, list connected services, and initiate new OAuth connections. Use when a workflow needs third-party service access (Google, Slack, GitHub, etc.). In MCP/headless mode, returns an authorization URL the user must open in their browser to complete the OAuth flow.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_knowledge', - agentId: 'knowledge', - description: - 'Manage knowledge bases for RAG-powered document retrieval. Supports listing, creating, updating, and deleting knowledge bases. Knowledge bases can be attached to agent blocks for context-aware responses.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_table', - agentId: 'table', - description: - 'Manage user-defined tables for structured data storage. Supports creating tables with typed schemas, inserting/updating/deleting rows, querying with filters and sorting, and batch operations.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_job', - agentId: 'scheduled_task', - description: - 'Manage scheduled tasks. Supports creating, listing, updating, pausing, resuming, and deleting scheduled tasks that run prompts against Sim on a schedule or at a specific time.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_agent', - agentId: 'agent', - description: - 'Manage custom tools, MCP server connections, and skills for agent blocks. Supports creating, editing, deleting, and listing custom JavaScript tools, external MCP server connections, and workspace skills. Can also research external MCP tools and add deployed workflows as MCP tools.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_info', - agentId: 'info', - description: - "Inspect a workflow's blocks, connections, outputs, variables, and metadata. Use for questions about the Sim platform itself — how blocks work, what integrations are available, platform concepts, etc. Provide workflowId when you want results scoped to a specific workflow.", - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'sim_research', - agentId: 'research', - description: - 'Research external APIs and documentation. Use when you need to understand third-party services, external APIs, authentication flows, or data formats OUTSIDE of Sim. For questions about Sim itself, use sim_info instead.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true, openWorldHint: true }, - }, - { - name: 'sim_superagent', - agentId: 'superagent', - description: - 'Execute direct actions NOW: send an email, post to Slack, make an API call, etc. Use when the user wants to DO something immediately rather than build a workflow for it.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: true, openWorldHint: true }, - }, - { - name: 'sim_platform', - agentId: 'tour', - description: - 'Get help with Sim platform navigation, keyboard shortcuts, and UI actions. Use when the user asks "how do I..." about the Sim editor, wants keyboard shortcuts, or needs to know what actions are available in the UI.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true }, - }, -] diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts new file mode 100644 index 00000000000..5ee25a69959 --- /dev/null +++ b/apps/sim/lib/copilot/tools/tool-display.ts @@ -0,0 +1,223 @@ +import { stripVersionSuffix } from '@sim/utils/string' + +/** + * Single source of truth for copilot tool-call display titles. + * + * The mothership (Go) no longer emits any presentation metadata on the stream — + * tool-call titles are derived entirely here, keyed by tool name (plus arguments + * for the dynamic cases). The live client render layer (see + * `home/hooks/stream/stream-helpers.ts`) wraps this with workspace/block-name + * enrichment for the run_* tools; every other surface (server persistence, + * transcript replay, fallback rendering) calls `getToolDisplayTitle` directly. + * + * Icons are likewise client-owned — see `getToolIcon` in the message-content + * utils. Nothing about tool presentation lives on the Go side anymore. + */ + +type ToolArgs = Record | undefined + +function stringArg(args: ToolArgs, key: string): string { + const value = args?.[key] + return typeof value === 'string' ? value.trim() : '' +} + +function firstStringArg(args: ToolArgs, ...keys: string[]): string { + for (const key of keys) { + const value = stringArg(args, key) + if (value) return value + } + return '' +} + +function stringArrayArg(args: ToolArgs, key: string): string[] { + const value = args?.[key] + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) +} + +function nestedStringArg(args: ToolArgs, parentKey: string, ...keys: string[]): string { + const parent = args?.[parentKey] + if (!parent || typeof parent !== 'object') return '' + return firstStringArg(parent as Record, ...keys) +} + +function operationTitle( + args: ToolArgs, + placeholder: string, + labels: Record +): string { + const operation = stringArg(args, 'operation') + return labels[operation] ?? placeholder +} + +function isWorkflowArtifactPath(path: string, filename: string): boolean { + const trimmed = path.trim() + return trimmed.startsWith('workflows/') && trimmed.endsWith(`/${filename}`) +} + +function workspaceFileTitle(args: ToolArgs): string { + const title = stringArg(args, 'title') + if (!title) return '' + const verbByOperation: Record = { + create: 'Creating', + append: 'Adding', + patch: 'Editing', + update: 'Writing', + rename: 'Renaming', + delete: 'Deleting', + } + const verb = verbByOperation[stringArg(args, 'operation')] ?? 'Writing' + return `${verb} ${title}` +} + +/** Static fallback titles for tools without an argument-aware title. */ +const TOOL_TITLES: Record = { + read: 'Reading file', + search_library_docs: 'Searching library docs', + user_memory: 'Accessing memory', + user_table: 'Managing table', + workspace_file: 'Editing file', + edit_content: 'Applying file content', + create_workflow: 'Creating workflow', + edit_workflow: 'Editing workflow', + knowledge_base: 'Managing knowledge base', + open_resource: 'Opening resource', + generate_image: 'Generating image', + generate_video: 'Generating video', + generate_audio: 'Generating audio', + ffmpeg: 'Processing media', + manage_folder: 'Folder action', + // Subagent trigger tools, when surfaced as a tool call. + workflow: 'Workflow Agent', + run: 'Run Agent', + deploy: 'Deploy Agent', + auth: 'Auth Agent', + knowledge: 'Knowledge Agent', + table: 'Table Agent', + scheduled_task: 'Scheduled Task Agent', + agent: 'Tools Agent', + research: 'Research Agent', + media: 'Media Agent', + superagent: 'Executing action', +} + +/** + * Final fallback: humanize a raw tool name (e.g. `manage_folder` -> "Manage + * Folder"), matching the legacy client humanizer so labels never render blank. + */ +export function humanizeToolName(name: string): string { + const words = stripVersionSuffix(name).split('_').filter(Boolean) + if (words.length === 0) return name + return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') +} + +/** + * Resolve a tool-call display title from its name and arguments. Argument-aware + * cases come first, then the static map, then a humanized fallback. This never + * returns an empty string. + */ +export function getToolDisplayTitle(name: string, args?: Record): string { + switch (name) { + case 'search_online': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Searching online for ${target}` : 'Searching online' + } + case 'grep': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Searching for ${target}` : 'Searching' + } + case 'glob': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Finding ${target}` : 'Finding files' + } + case 'enrichment_run': { + const subject = nestedStringArg( + args, + 'inputs', + 'fullName', + 'companyName', + 'domain', + 'email', + 'companyDomain' + ) + return subject ? `Searching for ${subject}` : 'Searching' + } + case 'scrape_page': { + const url = stringArg(args, 'url') + return url ? `Scraping ${url}` : 'Scraping page' + } + case 'crawl_website': { + const url = stringArg(args, 'url') + return url ? `Crawling ${url}` : 'Crawling website' + } + case 'get_page_contents': { + const urls = stringArrayArg(args, 'urls') + if (urls.length === 1) return `Getting ${urls[0]}` + if (urls.length > 1) return `Getting ${urls.length} pages` + return 'Getting page contents' + } + case 'manage_custom_tool': + return operationTitle(args, 'Custom tool action', { + add: 'Creating custom tool', + edit: 'Updating custom tool', + delete: 'Deleting custom tool', + list: 'Listing custom tools', + }) + case 'manage_mcp_tool': + return operationTitle(args, 'MCP server action', { + add: 'Creating MCP server', + edit: 'Updating MCP server', + delete: 'Deleting MCP server', + list: 'Listing MCP servers', + }) + case 'manage_skill': + return operationTitle(args, 'Skill action', { + add: 'Creating skill', + edit: 'Updating skill', + delete: 'Deleting skill', + list: 'Listing skills', + }) + case 'manage_scheduled_task': + return operationTitle(args, 'Scheduled task action', { + create: 'Creating scheduled task', + get: 'Getting scheduled task', + update: 'Updating scheduled task', + delete: 'Deleting scheduled task', + list: 'Listing scheduled tasks', + }) + case 'manage_credential': + return operationTitle(args, 'Credential action', { + rename: 'Renaming credential', + delete: 'Deleting credential', + }) + case 'manage_folder': + return operationTitle(args, 'Folder action', { + create: 'Creating folder', + rename: 'Renaming folder', + move: 'Moving folder', + delete: 'Deleting folder', + }) + case 'run_workflow': + case 'run_from_block': + case 'run_workflow_until_block': + return 'Running workflow' + case 'query_logs': { + const workflowName = stringArg(args, 'workflowName') + return workflowName ? `Querying logs for ${workflowName}` : 'Querying logs' + } + case 'read': { + if (isWorkflowArtifactPath(stringArg(args, 'path'), 'lint.json')) { + return 'Validating workflow state' + } + break + } + case 'workspace_file': + case 'function_execute': { + const title = name === 'workspace_file' ? workspaceFileTitle(args) : stringArg(args, 'title') + if (title) return title + break + } + } + + return TOOL_TITLES[name] ?? humanizeToolName(name) +} diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index b5a8701421d..f2e7f2fb477 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -7,26 +7,41 @@ import { DYNAMIC_MODEL_PROVIDERS, PROVIDER_DEFINITIONS } from '@/providers/model import type { ToolConfig } from '@/tools/types' /** - * Serialize workflow metadata for VFS meta.json + * Serialize workflow metadata for VFS meta.json. + * + * `locked` is the EFFECTIVE lock — true when the workflow is locked directly or + * sits inside a locked folder. A locked workflow cannot be edited, moved, + * renamed, or deleted (mutations are rejected server-side with a 423). The + * mothership should read this before attempting any workflow mutation. + * `inheritedFolderLock` carries the resolved containing-folder lock (the + * caller computes folder inheritance; see workspace-vfs materializeWorkflows). */ -export function serializeWorkflowMeta(wf: { - id: string - name: string - description?: string | null - folderId?: string | null - isDeployed: boolean - deployedAt?: Date | null - runCount: number - lastRunAt?: Date | null - createdAt: Date - updatedAt: Date -}): string { +export function serializeWorkflowMeta( + wf: { + id: string + name: string + description?: string | null + folderId?: string | null + isDeployed: boolean + deployedAt?: Date | null + runCount: number + lastRunAt?: Date | null + createdAt: Date + updatedAt: Date + locked?: boolean + }, + options?: { inheritedFolderLock?: boolean } +): string { + const directLock = wf.locked ?? false + const locked = directLock || (options?.inheritedFolderLock ?? false) return JSON.stringify( { id: wf.id, name: wf.name, description: wf.description || undefined, folderId: wf.folderId || undefined, + locked, + lockedBy: locked ? (directLock ? 'workflow' : 'folder') : undefined, isDeployed: wf.isDeployed, deployedAt: wf.deployedAt?.toISOString(), runCount: wf.runCount, diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 3b289b41081..0038755e603 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -1016,6 +1016,37 @@ export class WorkspaceVFS { return buildVfsFolderPathMap(folders) } + /** + * Resolve the set of folder IDs that are effectively locked — locked directly + * or via a locked ancestor folder. A workflow inside any of these folders is + * itself immutable, so its meta.json must report `locked: true`. Mirrors the + * folder-chain walk in `@sim/workflow-authz` getFolderLockStatus, but resolves + * the whole workspace in memory to avoid a per-workflow DB round trip. + */ + private computeLockedFolderIds( + folders: Array<{ folderId: string; parentId: string | null; locked: boolean }> + ): Set { + const byId = new Map(folders.map((f) => [f.folderId, f])) + const lockedFolderIds = new Set() + + for (const folder of folders) { + let current: string | null = folder.folderId + const visited = new Set() + while (current && !visited.has(current)) { + visited.add(current) + const node = byId.get(current) + if (!node) break + if (node.locked) { + lockedFolderIds.add(folder.folderId) + break + } + current = node.parentId + } + } + + return lockedFolderIds + } + /** * Materialize all workflows using the shared listWorkflows function. * Workflows are nested under their folder paths in the VFS: @@ -1031,6 +1062,7 @@ export class WorkspaceVFS { ]) const folderPaths = this.buildFolderPaths(folderRows) + const lockedFolderIds = this.computeLockedFolderIds(folderRows) // NOTE: materialization is a pure READ. Alias backing (changelog/plan // folders + files) is ensured at write time — workflow create/rename @@ -1056,7 +1088,8 @@ export class WorkspaceVFS { const prefix = `${canonicalWorkflowVfsDir({ name: wf.name, folderPath })}/` const workflowPath = prefix.replace(/\/$/, '') - this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf)) + const inheritedFolderLock = wf.folderId ? lockedFolderIds.has(wf.folderId) : false + this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf, { inheritedFolderLock })) if (workflowArtifactsEnabled) { const changelog = findWorkspaceFileRecord( diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts index 219c194e080..2f20e012a99 100644 --- a/apps/sim/lib/workflows/utils.ts +++ b/apps/sim/lib/workflows/utils.ts @@ -608,6 +608,7 @@ export async function listFolders(workspaceId: string) { folderName: workflowFolder.name, parentId: workflowFolder.parentId, sortOrder: workflowFolder.sortOrder, + locked: workflowFolder.locked, }) .from(workflowFolder) .where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt))) From 58312a10e504d1034b42df33d49213b09e13a86b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 18 Jun 2026 12:31:54 -0700 Subject: [PATCH 03/16] improvement(misc): add more sportmonks tools, improvestreaming ux (#5129) --- .../docs/en/integrations/sportmonks.mdx | 6950 +++++++++++++++-- .../components/agent-group/agent-group.tsx | 27 +- .../message-content/components/index.ts | 1 - .../components/thinking-block/index.ts | 1 - .../thinking-block/thinking-block.tsx | 108 - .../message-content/message-content.tsx | 85 +- apps/sim/blocks/blocks/sportmonks.ts | 2379 +++++- apps/sim/lib/integrations/integrations.json | 852 +- apps/sim/tools/registry.ts | 438 +- .../tools/sportmonks_core/get_continents.ts | 6 - .../sportmonks_core/get_entity_filters.ts | 61 + .../sim/tools/sportmonks_core/get_my_usage.ts | 92 + .../sportmonks_core/get_type_by_entity.ts | 64 + apps/sim/tools/sportmonks_core/index.ts | 4 + .../tools/sportmonks_core/search_cities.ts | 12 + .../tools/sportmonks_core/search_countries.ts | 14 +- .../tools/sportmonks_core/search_regions.ts | 115 + apps/sim/tools/sportmonks_core/types.ts | 78 +- .../sportmonks_football/expected_by_player.ts | 106 + .../sportmonks_football/expected_by_team.ts | 106 + .../get_all_commentaries.ts | 104 + .../sportmonks_football/get_all_fixtures.ts | 105 + .../sportmonks_football/get_all_players.ts | 105 + .../sportmonks_football/get_all_rivals.ts | 104 + .../sportmonks_football/get_all_teams.ts | 104 + .../get_all_transfer_rumours.ts | 106 + .../sportmonks_football/get_all_transfers.ts | 105 + .../get_brackets_by_season.ts | 80 + .../tools/sportmonks_football/get_coach.ts | 90 + .../tools/sportmonks_football/get_coaches.ts | 104 + .../get_coaches_by_country.ts | 116 + .../get_commentaries_by_fixture.ts | 84 + .../get_current_leagues_by_team.ts | 116 + .../get_expected_lineups_by_player.ts | 115 + .../get_expected_lineups_by_team.ts | 115 + .../get_extended_team_squad.ts | 89 + .../get_fixtures_by_date_range_for_team.ts | 132 + .../get_fixtures_by_ids.ts | 90 + .../get_grouped_standings_by_round.ts | 87 + .../sportmonks_football/get_latest_coaches.ts | 106 + .../get_latest_fixtures.ts | 80 + .../get_latest_livescores.ts | 80 + .../sportmonks_football/get_latest_players.ts | 80 + .../sportmonks_football/get_latest_totw.ts | 84 + .../get_latest_transfers.ts | 106 + .../get_leagues_by_country.ts | 116 + .../get_leagues_by_date.ts | 116 + .../get_leagues_by_team.ts | 116 + .../sportmonks_football/get_live_leagues.ts | 105 + .../get_live_probabilities.ts | 102 + .../get_live_probabilities_by_fixture.ts | 110 + .../get_live_standings_by_league.ts | 90 + .../sportmonks_football/get_match_facts.ts | 104 + .../get_match_facts_by_date_range.ts | 124 + .../get_match_facts_by_fixture.ts | 115 + .../get_match_facts_by_league.ts | 115 + .../get_past_fixtures_by_tv_station.ts | 115 + .../get_players_by_country.ts | 116 + .../sportmonks_football/get_postmatch_news.ts | 106 + .../get_postmatch_news_by_season.ts | 115 + .../get_predictability_by_league.ts | 115 + .../sportmonks_football/get_prematch_news.ts | 105 + .../get_prematch_news_by_season.ts | 115 + .../get_prematch_news_upcoming.ts | 105 + .../sportmonks_football/get_probabilities.ts | 106 + .../get_probabilities_by_fixture.ts | 115 + .../tools/sportmonks_football/get_referee.ts | 89 + .../tools/sportmonks_football/get_referees.ts | 104 + .../get_referees_by_country.ts | 116 + .../get_referees_by_season.ts | 116 + .../sportmonks_football/get_rivals_by_team.ts | 83 + .../tools/sportmonks_football/get_round.ts | 90 + .../get_round_statistics.ts | 115 + .../tools/sportmonks_football/get_rounds.ts | 105 + .../get_rounds_by_season.ts | 89 + .../get_schedules_by_season.ts | 73 + .../get_schedules_by_season_and_team.ts | 82 + .../get_schedules_by_team.ts | 73 + .../tools/sportmonks_football/get_season.ts | 90 + .../tools/sportmonks_football/get_seasons.ts | 104 + .../get_seasons_by_team.ts | 89 + .../tools/sportmonks_football/get_stage.ts | 90 + .../get_stage_statistics.ts | 115 + .../tools/sportmonks_football/get_stages.ts | 105 + .../get_stages_by_season.ts | 89 + .../get_standing_corrections_by_season.ts | 89 + .../sportmonks_football/get_standings.ts | 105 + .../get_standings_by_round.ts | 90 + .../tools/sportmonks_football/get_state.ts | 77 + .../tools/sportmonks_football/get_states.ts | 93 + .../sportmonks_football/get_team_rankings.ts | 98 + .../get_team_rankings_by_date.ts | 109 + .../get_team_rankings_by_team.ts | 109 + .../get_team_squad_by_season.ts | 98 + .../get_teams_by_country.ts | 115 + .../get_teams_by_season.ts | 115 + .../get_topscorers_by_stage.ts | 116 + .../sim/tools/sportmonks_football/get_totw.ts | 96 + .../sportmonks_football/get_totw_by_round.ts | 84 + .../tools/sportmonks_football/get_transfer.ts | 90 + .../get_transfer_rumour.ts | 90 + .../get_transfer_rumours_between_dates.ts | 125 + .../get_transfer_rumours_by_player.ts | 116 + .../get_transfer_rumours_by_team.ts | 116 + .../get_transfers_between_dates.ts | 125 + .../get_transfers_by_player.ts | 116 + .../get_transfers_by_team.ts | 116 + .../sportmonks_football/get_tv_station.ts | 89 + .../sportmonks_football/get_tv_stations.ts | 104 + .../get_tv_stations_by_fixture.ts | 115 + .../get_upcoming_fixtures_by_market.ts | 115 + .../get_upcoming_fixtures_by_tv_station.ts | 115 + .../sportmonks_football/get_value_bets.ts | 99 + .../get_value_bets_by_fixture.ts | 109 + .../tools/sportmonks_football/get_venue.ts | 90 + .../tools/sportmonks_football/get_venues.ts | 104 + .../get_venues_by_season.ts | 89 + apps/sim/tools/sportmonks_football/index.ts | 107 + .../sportmonks_football/search_coaches.ts | 115 + .../sportmonks_football/search_fixtures.ts | 116 + .../sportmonks_football/search_leagues.ts | 116 + .../sportmonks_football/search_referees.ts | 115 + .../sportmonks_football/search_rounds.ts | 115 + .../sportmonks_football/search_seasons.ts | 115 + .../sportmonks_football/search_stages.ts | 115 + .../sportmonks_football/search_venues.ts | 115 + apps/sim/tools/sportmonks_football/types.ts | 960 ++- .../sportmonks_motorsport/get_all_fixtures.ts | 105 + .../get_current_leagues_by_team.ts | 115 + .../get_driver_standings.ts | 105 + .../get_drivers_by_country.ts | 115 + .../get_drivers_by_season.ts | 115 + .../get_fixtures_by_date_range.ts | 123 + .../get_fixtures_by_ids.ts | 117 + .../get_laps_by_fixture_and_driver.ts | 97 + .../get_laps_by_fixture_and_lap.ts | 97 + .../get_latest_laps_by_fixture.ts | 91 + .../get_latest_pitstops_by_fixture.ts | 91 + .../get_latest_stints_by_fixture.ts | 91 + .../get_latest_updated_drivers.ts | 105 + .../get_latest_updated_fixtures.ts | 106 + .../tools/sportmonks_motorsport/get_league.ts | 89 + .../sportmonks_motorsport/get_leagues.ts | 104 + .../get_leagues_by_country.ts | 115 + .../get_leagues_by_date.ts | 116 + .../get_leagues_by_live.ts | 105 + .../get_leagues_by_team.ts | 116 + .../get_pitstops_by_fixture_and_driver.ts | 97 + .../get_pitstops_by_fixture_and_lap.ts | 97 + .../get_race_results_by_season_and_driver.ts | 124 + .../get_race_results_by_season_and_team.ts | 124 + .../get_schedules_by_season.ts | 117 + .../tools/sportmonks_motorsport/get_season.ts | 89 + .../sportmonks_motorsport/get_seasons.ts | 104 + .../tools/sportmonks_motorsport/get_stage.ts | 90 + .../tools/sportmonks_motorsport/get_stages.ts | 105 + .../get_stages_by_season.ts | 117 + .../tools/sportmonks_motorsport/get_state.ts | 89 + .../tools/sportmonks_motorsport/get_states.ts | 104 + .../get_stints_by_fixture.ts | 91 + .../get_stints_by_fixture_and_driver.ts | 97 + .../get_stints_by_fixture_and_stint.ts | 97 + .../get_team_standings.ts | 105 + .../get_teams_by_country.ts | 116 + .../get_teams_by_season.ts | 116 + .../tools/sportmonks_motorsport/get_venue.ts | 89 + .../tools/sportmonks_motorsport/get_venues.ts | 104 + .../get_venues_by_season.ts | 116 + apps/sim/tools/sportmonks_motorsport/index.ts | 45 + .../sportmonks_motorsport/search_drivers.ts | 6 + .../sportmonks_motorsport/search_leagues.ts | 115 + .../sportmonks_motorsport/search_stages.ts | 116 + .../sportmonks_motorsport/search_teams.ts | 115 + .../sportmonks_motorsport/search_venues.ts | 115 + apps/sim/tools/sportmonks_motorsport/types.ts | 256 + .../get_all_historical_odds.ts | 106 + .../sportmonks_odds/get_all_inplay_odds.ts | 104 + .../sportmonks_odds/get_all_pre_match_odds.ts | 106 + .../sportmonks_odds/get_all_premium_odds.ts | 105 + .../get_bookmaker_event_ids_by_fixture.ts | 104 + .../get_bookmakers_by_fixture.ts | 103 + .../get_inplay_odds_by_fixture.ts | 4 +- ...et_inplay_odds_by_fixture_and_bookmaker.ts | 97 + .../get_inplay_odds_by_fixture_and_market.ts | 97 + .../get_last_updated_inplay_odds.ts | 79 + .../get_last_updated_pre_match_odds.ts | 80 + .../get_pre_match_odds_by_fixture.ts | 4 +- ...pre_match_odds_by_fixture_and_bookmaker.ts | 97 + ...et_pre_match_odds_by_fixture_and_market.ts | 97 + .../get_premium_odds_by_fixture.ts | 90 + ...t_premium_odds_by_fixture_and_bookmaker.ts | 97 + .../get_premium_odds_by_fixture_and_market.ts | 97 + .../get_updated_historical_odds_between.ts | 123 + .../get_updated_premium_odds_between.ts | 123 + apps/sim/tools/sportmonks_odds/index.ts | 17 + .../sportmonks_odds/search_bookmakers.ts | 6 + .../tools/sportmonks_odds/search_markets.ts | 6 + apps/sim/tools/sportmonks_odds/types.ts | 215 +- 198 files changed, 29043 insertions(+), 1423 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx create mode 100644 apps/sim/tools/sportmonks_core/get_entity_filters.ts create mode 100644 apps/sim/tools/sportmonks_core/get_my_usage.ts create mode 100644 apps/sim/tools/sportmonks_core/get_type_by_entity.ts create mode 100644 apps/sim/tools/sportmonks_core/search_regions.ts create mode 100644 apps/sim/tools/sportmonks_football/expected_by_player.ts create mode 100644 apps/sim/tools/sportmonks_football/expected_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_commentaries.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_fixtures.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_players.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_rivals.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_teams.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_transfer_rumours.ts create mode 100644 apps/sim/tools/sportmonks_football/get_all_transfers.ts create mode 100644 apps/sim/tools/sportmonks_football/get_brackets_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_coach.ts create mode 100644 apps/sim/tools/sportmonks_football/get_coaches.ts create mode 100644 apps/sim/tools/sportmonks_football/get_coaches_by_country.ts create mode 100644 apps/sim/tools/sportmonks_football/get_commentaries_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_current_leagues_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_expected_lineups_by_player.ts create mode 100644 apps/sim/tools/sportmonks_football/get_expected_lineups_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_extended_team_squad.ts create mode 100644 apps/sim/tools/sportmonks_football/get_fixtures_by_date_range_for_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_fixtures_by_ids.ts create mode 100644 apps/sim/tools/sportmonks_football/get_grouped_standings_by_round.ts create mode 100644 apps/sim/tools/sportmonks_football/get_latest_coaches.ts create mode 100644 apps/sim/tools/sportmonks_football/get_latest_fixtures.ts create mode 100644 apps/sim/tools/sportmonks_football/get_latest_livescores.ts create mode 100644 apps/sim/tools/sportmonks_football/get_latest_players.ts create mode 100644 apps/sim/tools/sportmonks_football/get_latest_totw.ts create mode 100644 apps/sim/tools/sportmonks_football/get_latest_transfers.ts create mode 100644 apps/sim/tools/sportmonks_football/get_leagues_by_country.ts create mode 100644 apps/sim/tools/sportmonks_football/get_leagues_by_date.ts create mode 100644 apps/sim/tools/sportmonks_football/get_leagues_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_live_leagues.ts create mode 100644 apps/sim/tools/sportmonks_football/get_live_probabilities.ts create mode 100644 apps/sim/tools/sportmonks_football/get_live_probabilities_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_live_standings_by_league.ts create mode 100644 apps/sim/tools/sportmonks_football/get_match_facts.ts create mode 100644 apps/sim/tools/sportmonks_football/get_match_facts_by_date_range.ts create mode 100644 apps/sim/tools/sportmonks_football/get_match_facts_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_match_facts_by_league.ts create mode 100644 apps/sim/tools/sportmonks_football/get_past_fixtures_by_tv_station.ts create mode 100644 apps/sim/tools/sportmonks_football/get_players_by_country.ts create mode 100644 apps/sim/tools/sportmonks_football/get_postmatch_news.ts create mode 100644 apps/sim/tools/sportmonks_football/get_postmatch_news_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_predictability_by_league.ts create mode 100644 apps/sim/tools/sportmonks_football/get_prematch_news.ts create mode 100644 apps/sim/tools/sportmonks_football/get_prematch_news_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_prematch_news_upcoming.ts create mode 100644 apps/sim/tools/sportmonks_football/get_probabilities.ts create mode 100644 apps/sim/tools/sportmonks_football/get_probabilities_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_referee.ts create mode 100644 apps/sim/tools/sportmonks_football/get_referees.ts create mode 100644 apps/sim/tools/sportmonks_football/get_referees_by_country.ts create mode 100644 apps/sim/tools/sportmonks_football/get_referees_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_rivals_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_round.ts create mode 100644 apps/sim/tools/sportmonks_football/get_round_statistics.ts create mode 100644 apps/sim/tools/sportmonks_football/get_rounds.ts create mode 100644 apps/sim/tools/sportmonks_football/get_rounds_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_schedules_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_schedules_by_season_and_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_schedules_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_seasons.ts create mode 100644 apps/sim/tools/sportmonks_football/get_seasons_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_stage.ts create mode 100644 apps/sim/tools/sportmonks_football/get_stage_statistics.ts create mode 100644 apps/sim/tools/sportmonks_football/get_stages.ts create mode 100644 apps/sim/tools/sportmonks_football/get_stages_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_standing_corrections_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_standings.ts create mode 100644 apps/sim/tools/sportmonks_football/get_standings_by_round.ts create mode 100644 apps/sim/tools/sportmonks_football/get_state.ts create mode 100644 apps/sim/tools/sportmonks_football/get_states.ts create mode 100644 apps/sim/tools/sportmonks_football/get_team_rankings.ts create mode 100644 apps/sim/tools/sportmonks_football/get_team_rankings_by_date.ts create mode 100644 apps/sim/tools/sportmonks_football/get_team_rankings_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_team_squad_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_teams_by_country.ts create mode 100644 apps/sim/tools/sportmonks_football/get_teams_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/get_topscorers_by_stage.ts create mode 100644 apps/sim/tools/sportmonks_football/get_totw.ts create mode 100644 apps/sim/tools/sportmonks_football/get_totw_by_round.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfer.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfer_rumour.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfer_rumours_between_dates.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfer_rumours_by_player.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfer_rumours_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfers_between_dates.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfers_by_player.ts create mode 100644 apps/sim/tools/sportmonks_football/get_transfers_by_team.ts create mode 100644 apps/sim/tools/sportmonks_football/get_tv_station.ts create mode 100644 apps/sim/tools/sportmonks_football/get_tv_stations.ts create mode 100644 apps/sim/tools/sportmonks_football/get_tv_stations_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_market.ts create mode 100644 apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_tv_station.ts create mode 100644 apps/sim/tools/sportmonks_football/get_value_bets.ts create mode 100644 apps/sim/tools/sportmonks_football/get_value_bets_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_football/get_venue.ts create mode 100644 apps/sim/tools/sportmonks_football/get_venues.ts create mode 100644 apps/sim/tools/sportmonks_football/get_venues_by_season.ts create mode 100644 apps/sim/tools/sportmonks_football/search_coaches.ts create mode 100644 apps/sim/tools/sportmonks_football/search_fixtures.ts create mode 100644 apps/sim/tools/sportmonks_football/search_leagues.ts create mode 100644 apps/sim/tools/sportmonks_football/search_referees.ts create mode 100644 apps/sim/tools/sportmonks_football/search_rounds.ts create mode 100644 apps/sim/tools/sportmonks_football/search_seasons.ts create mode 100644 apps/sim/tools/sportmonks_football/search_stages.ts create mode 100644 apps/sim/tools/sportmonks_football/search_venues.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_all_fixtures.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_current_leagues_by_team.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_driver_standings.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_drivers_by_country.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_drivers_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date_range.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_fixtures_by_ids.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_driver.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_lap.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_latest_laps_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_latest_pitstops_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_latest_stints_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_latest_updated_drivers.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_latest_updated_fixtures.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_league.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_leagues.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_leagues_by_country.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_leagues_by_date.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_leagues_by_live.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_leagues_by_team.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_driver.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_lap.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_driver.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_team.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_schedules_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_seasons.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_stage.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_stages.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_stages_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_state.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_states.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_driver.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_stint.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_team_standings.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_teams_by_country.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_teams_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_venue.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_venues.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/get_venues_by_season.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/search_leagues.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/search_stages.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/search_teams.ts create mode 100644 apps/sim/tools/sportmonks_motorsport/search_venues.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_all_historical_odds.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_all_inplay_odds.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_all_pre_match_odds.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_all_premium_odds.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_bookmaker_event_ids_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_bookmakers_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_bookmaker.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_market.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_last_updated_inplay_odds.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_last_updated_pre_match_odds.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_bookmaker.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_market.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_bookmaker.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_market.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_updated_historical_odds_between.ts create mode 100644 apps/sim/tools/sportmonks_odds/get_updated_premium_odds_between.ts diff --git a/apps/docs/content/docs/en/integrations/sportmonks.mdx b/apps/docs/content/docs/en/integrations/sportmonks.mdx index 21383de4d6f..f3d3d673742 100644 --- a/apps/docs/content/docs/en/integrations/sportmonks.mdx +++ b/apps/docs/content/docs/en/integrations/sportmonks.mdx @@ -12,152 +12,118 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions -Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, teams, squads, players, standings, and topscorers. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones. +Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, seasons, stages, rounds, teams, squads, players, coaches, referees, venues, standings, topscorers, transfers, schedules, commentaries, TV stations, rivals, expected goals (xG), and predictions. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones. ## Actions -### `sportmonks_football_get_livescores` +### `sportmonks_football_expected_by_player` -Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks +Retrieve lineup-level expected goals (xG) values per player from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | -| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;player;team;type\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of live fixture objects | -| ↳ `id` | number | Unique id of the fixture | -| ↳ `sport_id` | number | Sport the fixture is played at | -| ↳ `league_id` | number | League the fixture is played in | -| ↳ `season_id` | number | Season the fixture is played in | -| ↳ `stage_id` | number | Stage the fixture is played in | -| ↳ `group_id` | number | Group the fixture is played in | -| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | -| ↳ `round_id` | number | Round the fixture is played in | -| ↳ `state_id` | number | State \(status\) of the fixture | -| ↳ `venue_id` | number | Venue the fixture is played at | -| ↳ `name` | string | Name of the fixture \(participants\) | -| ↳ `starting_at` | string | Datetime the fixture starts | -| ↳ `result_info` | string | Final result summary | -| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Length of the fixture in minutes | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Whether odds are available | -| ↳ `has_premium_odds` | boolean | Whether premium odds are available | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `expected` | array | Array of player-level expected goals \(xG\) entries | +| ↳ `id` | number | Unique id of the expected value | +| ↳ `fixture_id` | number | Fixture related to the value | +| ↳ `player_id` | number | Player related to the value | +| ↳ `team_id` | number | Team related to the value | +| ↳ `lineup_id` | number | Lineup record the player relates to | +| ↳ `type_id` | number | Type of the expected value | +| ↳ `data` | object | The expected value payload | +| ↳ `value` | number | The xG value | -### `sportmonks_football_get_inplay_livescores` +### `sportmonks_football_expected_by_team` -Retrieve all fixtures that are currently being played (in-play) from Sportmonks +Retrieve fixture-level expected goals (xG) values per team from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | -| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;participant;type\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of in-play fixture objects | -| ↳ `id` | number | Unique id of the fixture | -| ↳ `sport_id` | number | Sport the fixture is played at | -| ↳ `league_id` | number | League the fixture is played in | -| ↳ `season_id` | number | Season the fixture is played in | -| ↳ `stage_id` | number | Stage the fixture is played in | -| ↳ `group_id` | number | Group the fixture is played in | -| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | -| ↳ `round_id` | number | Round the fixture is played in | -| ↳ `state_id` | number | State \(status\) of the fixture | -| ↳ `venue_id` | number | Venue the fixture is played at | -| ↳ `name` | string | Name of the fixture \(participants\) | -| ↳ `starting_at` | string | Datetime the fixture starts | -| ↳ `result_info` | string | Final result summary | -| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Length of the fixture in minutes | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Whether odds are available | -| ↳ `has_premium_odds` | boolean | Whether premium odds are available | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `expected` | array | Array of team-level expected goals \(xG\) entries | +| ↳ `id` | number | Unique id of the expected value | +| ↳ `fixture_id` | number | Fixture related to the value | +| ↳ `type_id` | number | Type of the expected value | +| ↳ `participant_id` | number | Team related to the expected value | +| ↳ `data` | object | The expected value payload | +| ↳ `value` | number | The xG value | +| ↳ `location` | string | Home or away | -### `sportmonks_football_get_fixtures_by_date` +### `sportmonks_football_get_all_commentaries` -Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks +Retrieve all textual commentaries available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;league\) | -| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;player\) | +| `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of fixture objects for the requested date | -| ↳ `id` | number | Unique id of the fixture | -| ↳ `sport_id` | number | Sport the fixture is played at | -| ↳ `league_id` | number | League the fixture is played in | -| ↳ `season_id` | number | Season the fixture is played in | -| ↳ `stage_id` | number | Stage the fixture is played in | -| ↳ `group_id` | number | Group the fixture is played in | -| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | -| ↳ `round_id` | number | Round the fixture is played in | -| ↳ `state_id` | number | State \(status\) of the fixture | -| ↳ `venue_id` | number | Venue the fixture is played at | -| ↳ `name` | string | Name of the fixture \(participants\) | -| ↳ `starting_at` | string | Datetime the fixture starts | -| ↳ `result_info` | string | Final result summary | -| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Length of the fixture in minutes | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Whether odds are available | -| ↳ `has_premium_odds` | boolean | Whether premium odds are available | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `commentaries` | array | Array of commentary entries | +| ↳ `id` | number | Unique id of the commentary | +| ↳ `fixture_id` | number | Fixture related to the commentary | +| ↳ `comment` | string | The commentary text | +| ↳ `minute` | number | Match minute of the comment | +| ↳ `extra_minute` | number | Extra \(injury\) minute of the comment | +| ↳ `is_goal` | boolean | Whether the comment is a goal | +| ↳ `is_important` | boolean | Whether the comment is important | +| ↳ `order` | number | Order of the comment | -### `sportmonks_football_get_fixtures_by_date_range` +### `sportmonks_football_get_all_fixtures` -Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max range is 100 days. +Retrieve all football fixtures available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `startDate` | string | Yes | Start date in YYYY-MM-DD format | -| `endDate` | string | Yes | End date in YYYY-MM-DD format \(max 100 days after start\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | -| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of fixture objects within the requested date range | +| `fixtures` | array | Array of fixture objects | | ↳ `id` | number | Unique id of the fixture | | ↳ `sport_id` | number | Sport the fixture is played at | | ↳ `league_id` | number | League the fixture is played in | @@ -179,392 +145,368 @@ Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max r | ↳ `has_premium_odds` | boolean | Whether premium odds are available | | ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | -### `sportmonks_football_get_fixture` +### `sportmonks_football_get_all_players` -Retrieve a single football fixture by its ID from Sportmonks +Retrieve all football players available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `fixtureId` | string | Yes | The unique id of the fixture | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events;lineups;statistics\) | -| `filters` | string | No | Filters to apply \(e.g. eventTypes:14\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. nationality;position\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order players by id \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixture` | object | The requested fixture object | -| ↳ `id` | number | Unique id of the fixture | -| ↳ `sport_id` | number | Sport the fixture is played at | -| ↳ `league_id` | number | League the fixture is played in | -| ↳ `season_id` | number | Season the fixture is played in | -| ↳ `stage_id` | number | Stage the fixture is played in | -| ↳ `group_id` | number | Group the fixture is played in | -| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | -| ↳ `round_id` | number | Round the fixture is played in | -| ↳ `state_id` | number | State \(status\) of the fixture | -| ↳ `venue_id` | number | Venue the fixture is played at | -| ↳ `name` | string | Name of the fixture \(participants\) | -| ↳ `starting_at` | string | Datetime the fixture starts | -| ↳ `result_info` | string | Final result summary | -| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Length of the fixture in minutes | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Whether odds are available | -| ↳ `has_premium_odds` | boolean | Whether premium odds are available | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `players` | array | Array of player objects | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | -### `sportmonks_football_get_head_to_head` +### `sportmonks_football_get_all_rivals` -Retrieve the head-to-head fixtures between two teams from Sportmonks +Retrieve all teams with their rivals information from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `team1` | string | Yes | The id of the first team | -| `team2` | string | Yes | The id of the second team | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team;rival\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of head-to-head fixture objects between the two teams | -| ↳ `id` | number | Unique id of the fixture | -| ↳ `sport_id` | number | Sport the fixture is played at | -| ↳ `league_id` | number | League the fixture is played in | -| ↳ `season_id` | number | Season the fixture is played in | -| ↳ `stage_id` | number | Stage the fixture is played in | -| ↳ `group_id` | number | Group the fixture is played in | -| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | -| ↳ `round_id` | number | Round the fixture is played in | -| ↳ `state_id` | number | State \(status\) of the fixture | -| ↳ `venue_id` | number | Venue the fixture is played at | -| ↳ `name` | string | Name of the fixture \(participants\) | -| ↳ `starting_at` | string | Datetime the fixture starts | -| ↳ `result_info` | string | Final result summary | -| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Length of the fixture in minutes | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Whether odds are available | -| ↳ `has_premium_odds` | boolean | Whether premium odds are available | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `rivals` | array | Array of rival relationships | +| ↳ `sport_id` | number | Sport of the rival | +| ↳ `team_id` | number | Team the rivalry belongs to | +| ↳ `rival_id` | number | Rival team id | -### `sportmonks_football_get_leagues` +### `sportmonks_football_get_all_teams` -Retrieve all football leagues available within your Sportmonks subscription +Retrieve all football teams available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order leagues \(asc or desc\) | +| `order` | string | No | Order teams by id \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leagues` | array | Array of league objects | -| ↳ `id` | number | Unique id of the league | -| ↳ `sport_id` | number | Sport of the league | -| ↳ `country_id` | number | Country of the league | -| ↳ `name` | string | Name of the league | -| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | -| ↳ `short_code` | string | Short code of the league | -| ↳ `image_path` | string | URL to the league logo | -| ↳ `type` | string | Type of the league | -| ↳ `sub_type` | string | Subtype of the league | -| ↳ `last_played_at` | string | Date the last fixture was played | -| ↳ `category` | number | Importance category of the league \(1-4\) | +| `teams` | array | Array of team objects | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | -### `sportmonks_football_get_league` +### `sportmonks_football_get_all_transfer_rumours` -Retrieve a single football league by its ID from Sportmonks +Retrieve all transfer rumours available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `leagueId` | string | Yes | The unique id of the league | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason;seasons\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `league` | object | The requested league object | -| ↳ `id` | number | Unique id of the league | -| ↳ `sport_id` | number | Sport of the league | -| ↳ `country_id` | number | Country of the league | -| ↳ `name` | string | Name of the league | -| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | -| ↳ `short_code` | string | Short code of the league | -| ↳ `image_path` | string | URL to the league logo | -| ↳ `type` | string | Type of the league | -| ↳ `sub_type` | string | Subtype of the league | -| ↳ `last_played_at` | string | Date the last fixture was played | -| ↳ `category` | number | Importance category of the league \(1-4\) | - -### `sportmonks_football_search_teams` - -Search for football teams by name from Sportmonks +| `transferRumours` | array | Array of transfer rumour objects | +| ↳ `id` | number | Unique id of the transfer rumour | +| ↳ `sport_id` | number | Sport of the transfer rumour | +| ↳ `player_id` | number | Player the rumour relates to | +| ↳ `position_id` | number | Position id of the player | +| ↳ `from_team_id` | number | Team the player would transfer from | +| ↳ `to_team_id` | number | Team the player would transfer to | +| ↳ `transfer_fee_id` | number | Transfer fee id of the rumour | +| ↳ `probability` | string | Probability of the rumour \(e.g. LOW\) | +| ↳ `source_name` | string | Name of the source of the rumour | +| ↳ `source_url` | string | URL of the source of the rumour | +| ↳ `amount` | number | Estimated transfer fee amount | +| ↳ `currency` | string | Currency of the amount | +| ↳ `date` | string | Date of the rumour | +| ↳ `type_id` | number | Type of the transfer rumour | + +### `sportmonks_football_get_all_transfers` + +Retrieve all transfers available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `query` | string | Yes | The team name to search for \(e.g. Celtic\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | -| `filters` | string | No | Filters to apply | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order teams by id \(asc or desc\) | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `teams` | array | Array of team objects matching the search query | -| ↳ `id` | number | Unique id of the team | -| ↳ `sport_id` | number | Sport of the team | -| ↳ `country_id` | number | Country of the team | -| ↳ `venue_id` | number | Home venue of the team | -| ↳ `gender` | string | Gender of the team | -| ↳ `name` | string | Name of the team | -| ↳ `short_code` | string | Short code of the team | -| ↳ `image_path` | string | URL to the team logo | -| ↳ `founded` | number | Founding year of the team | -| ↳ `type` | string | Type of the team | -| ↳ `placeholder` | boolean | Whether the team is a placeholder | -| ↳ `last_played_at` | string | Date and time of the last played match | - -### `sportmonks_football_get_team` - -Retrieve a single football team by its ID from Sportmonks +| `transfers` | array | Array of transfer objects | +| ↳ `id` | number | Unique id of the transfer | +| ↳ `sport_id` | number | Sport of the transfer | +| ↳ `player_id` | number | Player who transferred | +| ↳ `type_id` | number | Type of the transfer | +| ↳ `from_team_id` | number | Team the player transferred from | +| ↳ `to_team_id` | number | Team the player transferred to | +| ↳ `position_id` | number | Position id of the transfer | +| ↳ `detailed_position_id` | number | Detailed position id of the transfer | +| ↳ `date` | string | Date of the transfer | +| ↳ `career_ended` | boolean | Whether the transfer ended the career | +| ↳ `completed` | boolean | Whether the transfer is completed | +| ↳ `amount` | number | Transfer fee amount | + +### `sportmonks_football_get_brackets_by_season` + +Retrieve the knockout-stage tournament bracket (stages and progression edges) for a season ID #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `teamId` | string | Yes | The unique id of the team | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue;coaches;players.player\) | -| `filters` | string | No | Filters to apply | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `team` | object | The requested team object | -| ↳ `id` | number | Unique id of the team | -| ↳ `sport_id` | number | Sport of the team | -| ↳ `country_id` | number | Country of the team | -| ↳ `venue_id` | number | Home venue of the team | -| ↳ `gender` | string | Gender of the team | -| ↳ `name` | string | Name of the team | -| ↳ `short_code` | string | Short code of the team | -| ↳ `image_path` | string | URL to the team logo | -| ↳ `founded` | number | Founding year of the team | -| ↳ `type` | string | Type of the team | -| ↳ `placeholder` | boolean | Whether the team is a placeholder | -| ↳ `last_played_at` | string | Date and time of the last played match | +| `brackets` | json | Bracket object containing stages \(fixtures grouped by knockout round\) and edges \(progression paths between fixtures\) | -### `sportmonks_football_get_team_squad` +### `sportmonks_football_get_coach` -Retrieve the current domestic squad for a team by team ID from Sportmonks +Retrieve a single football coach by their ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `teamId` | string | Yes | The unique id of the team | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | +| `coachId` | string | Yes | The unique id of the coach | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams;statistics\) | | `filters` | string | No | Filters to apply | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `squad` | array | Array of squad entries for the team | -| ↳ `id` | number | Unique id of the squad record | -| ↳ `transfer_id` | number | Transfer id of the squad record | -| ↳ `player_id` | number | Player in the squad | -| ↳ `team_id` | number | Team of the squad | -| ↳ `position_id` | number | Position of the player in the squad | -| ↳ `detailed_position_id` | number | Detailed position of the player in the squad | -| ↳ `jersey_number` | number | Jersey number of the player | -| ↳ `start` | string | Start contract date of the player | -| ↳ `end` | string | End contract date of the player | - -### `sportmonks_football_search_players` - -Search for football players by name from Sportmonks +| `coach` | object | The requested coach object | +| ↳ `id` | number | Unique id of the coach | +| ↳ `player_id` | number | Player related to the coach | +| ↳ `sport_id` | number | Sport of the coach | +| ↳ `country_id` | number | Country of the coach | +| ↳ `nationality_id` | number | Nationality of the coach | +| ↳ `city_id` | number | Birth city of the coach | +| ↳ `common_name` | string | Common name of the coach | +| ↳ `firstname` | string | First name of the coach | +| ↳ `lastname` | string | Last name of the coach | +| ↳ `name` | string | Name of the coach | +| ↳ `display_name` | string | Display name of the coach | +| ↳ `image_path` | string | URL to the coach headshot | +| ↳ `height` | number | Height of the coach in cm | +| ↳ `weight` | number | Weight of the coach in kg | +| ↳ `date_of_birth` | string | Date of birth of the coach | +| ↳ `gender` | string | Gender of the coach | + +### `sportmonks_football_get_coaches` + +Retrieve all football coaches available within your Sportmonks subscription #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `query` | string | Yes | The player name to search for \(e.g. Tavernier\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team\) | -| `filters` | string | No | Filters to apply | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply \(e.g. coachCountries:462\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order players by id \(asc or desc\) | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `players` | array | Array of player objects matching the search query | -| ↳ `id` | number | Unique id of the player | -| ↳ `sport_id` | number | Sport of the player | -| ↳ `country_id` | number | Country of birth of the player | -| ↳ `nationality_id` | number | Nationality of the player | -| ↳ `city_id` | number | City of birth of the player | -| ↳ `position_id` | number | Position of the player | -| ↳ `detailed_position_id` | number | Detailed position of the player | -| ↳ `type_id` | number | Type of the player | -| ↳ `common_name` | string | Name the player is known for | -| ↳ `firstname` | string | First name of the player | -| ↳ `lastname` | string | Last name of the player | -| ↳ `name` | string | Name of the player | -| ↳ `display_name` | string | Display name of the player | -| ↳ `image_path` | string | URL to the player headshot | -| ↳ `height` | number | Height of the player in cm | -| ↳ `weight` | number | Weight of the player in kg | -| ↳ `date_of_birth` | string | Date of birth of the player | -| ↳ `gender` | string | Gender of the player | - -### `sportmonks_football_get_player` - -Retrieve a single football player by their ID from Sportmonks +| `coaches` | array | Array of coach objects | +| ↳ `id` | number | Unique id of the coach | +| ↳ `player_id` | number | Player related to the coach | +| ↳ `sport_id` | number | Sport of the coach | +| ↳ `country_id` | number | Country of the coach | +| ↳ `nationality_id` | number | Nationality of the coach | +| ↳ `city_id` | number | Birth city of the coach | +| ↳ `common_name` | string | Common name of the coach | +| ↳ `firstname` | string | First name of the coach | +| ↳ `lastname` | string | Last name of the coach | +| ↳ `name` | string | Name of the coach | +| ↳ `display_name` | string | Display name of the coach | +| ↳ `image_path` | string | URL to the coach headshot | +| ↳ `height` | number | Height of the coach in cm | +| ↳ `weight` | number | Weight of the coach in kg | +| ↳ `date_of_birth` | string | Date of birth of the coach | +| ↳ `gender` | string | Gender of the coach | + +### `sportmonks_football_get_coaches_by_country` + +Retrieve all coaches for a country ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `playerId` | string | Yes | The unique id of the player | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team;statistics\) | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | | `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `player` | object | The requested player object | -| ↳ `id` | number | Unique id of the player | -| ↳ `sport_id` | number | Sport of the player | -| ↳ `country_id` | number | Country of birth of the player | -| ↳ `nationality_id` | number | Nationality of the player | -| ↳ `city_id` | number | City of birth of the player | -| ↳ `position_id` | number | Position of the player | -| ↳ `detailed_position_id` | number | Detailed position of the player | -| ↳ `type_id` | number | Type of the player | -| ↳ `common_name` | string | Name the player is known for | -| ↳ `firstname` | string | First name of the player | -| ↳ `lastname` | string | Last name of the player | -| ↳ `name` | string | Name of the player | -| ↳ `display_name` | string | Display name of the player | -| ↳ `image_path` | string | URL to the player headshot | -| ↳ `height` | number | Height of the player in cm | -| ↳ `weight` | number | Weight of the player in kg | -| ↳ `date_of_birth` | string | Date of birth of the player | -| ↳ `gender` | string | Gender of the player | - -### `sportmonks_football_get_standings_by_season` - -Retrieve the full league standings table for a season by season ID from Sportmonks +| `coaches` | array | Array of coach objects for the country | +| ↳ `id` | number | Unique id of the coach | +| ↳ `player_id` | number | Player related to the coach | +| ↳ `sport_id` | number | Sport of the coach | +| ↳ `country_id` | number | Country of the coach | +| ↳ `nationality_id` | number | Nationality of the coach | +| ↳ `city_id` | number | Birth city of the coach | +| ↳ `common_name` | string | Common name of the coach | +| ↳ `firstname` | string | First name of the coach | +| ↳ `lastname` | string | Last name of the coach | +| ↳ `name` | string | Name of the coach | +| ↳ `display_name` | string | Display name of the coach | +| ↳ `image_path` | string | URL to the coach headshot | +| ↳ `height` | number | Height of the coach in cm | +| ↳ `weight` | number | Weight of the coach in kg | +| ↳ `date_of_birth` | string | Date of birth of the coach | +| ↳ `gender` | string | Gender of the coach | + +### `sportmonks_football_get_commentaries_by_fixture` + +Retrieve textual commentary for a fixture by fixture ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `seasonId` | string | Yes | The unique id of the season | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details;form\) | -| `filters` | string | No | Filters to apply \(e.g. standingStages:77453568\) | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;relatedPlayer\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `standings` | array | Array of standing entries for the season | -| ↳ `id` | number | Unique id of the standing | -| ↳ `participant_id` | number | Team related to the standing | -| ↳ `sport_id` | number | Sport related to the standing | -| ↳ `league_id` | number | League related to the standing | -| ↳ `season_id` | number | Season related to the standing | -| ↳ `stage_id` | number | Stage related to the standing | -| ↳ `group_id` | number | Group related to the standing | -| ↳ `round_id` | number | Round related to the standing | -| ↳ `standing_rule_id` | number | Standing rule related to the standing | -| ↳ `position` | number | Position of the team in the standing | -| ↳ `result` | string | Movement of the team in the standing | -| ↳ `points` | number | Points the team has gathered | +| `commentaries` | array | Array of commentary entries for the fixture | +| ↳ `id` | number | Unique id of the commentary | +| ↳ `fixture_id` | number | Fixture related to the commentary | +| ↳ `comment` | string | The commentary text | +| ↳ `minute` | number | Match minute of the comment | +| ↳ `extra_minute` | number | Extra \(injury\) minute of the comment | +| ↳ `is_goal` | boolean | Whether the comment is a goal | +| ↳ `is_important` | boolean | Whether the comment is important | +| ↳ `order` | number | Order of the comment | -### `sportmonks_football_get_topscorers_by_season` +### `sportmonks_football_get_current_leagues_by_team` -Retrieve the topscorers (goals, assists, cards) for a season by season ID from Sportmonks +Retrieve all current leagues for a team ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `seasonId` | string | Yes | The unique id of the season | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;participant;type\) | -| `filters` | string | No | Filters to apply \(e.g. seasontopscorerTypes:208\) | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order topscorers by position \(asc or desc\) | +| `order` | string | No | Order leagues \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `topscorers` | array | Array of topscorer entries for the season | -| ↳ `id` | number | Unique id of the topscorer record | -| ↳ `season_id` | number | Season related to the topscorer | -| ↳ `league_id` | number | League related to the topscorer | -| ↳ `stage_id` | number | Stage related to the topscorer | -| ↳ `player_id` | number | Player related to the topscorer | -| ↳ `participant_id` | number | Team related to the topscorer | -| ↳ `type_id` | number | Type of the topscorer \(goals, assists, cards\) | -| ↳ `position` | number | Position of the topscorer | -| ↳ `total` | number | Number of goals, assists or cards | +| `leagues` | array | Array of current league objects for the team | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | -### `sportmonks_motorsport_get_livescores` +### `sportmonks_football_get_expected_lineups_by_player` -Retrieve all live motorsport fixtures (sessions) from Sportmonks +Retrieve the premium expected lineups for a player ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | +| `playerId` | string | Yes | The unique id of the player | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fixture\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -574,150 +516,3468 @@ Retrieve all live motorsport fixtures (sessions) from Sportmonks | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of live motorsport fixture \(session\) objects | -| ↳ `id` | number | Unique id of the fixture \(session\) | -| ↳ `sport_id` | number | Sport of the fixture | -| ↳ `league_id` | number | League the fixture is held in | -| ↳ `season_id` | number | Season the fixture is held in | -| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | -| ↳ `group_id` | number | Not used in the Motorsport API | -| ↳ `aggregate_id` | number | Not used in the Motorsport API | -| ↳ `round_id` | number | Not used in the Motorsport API | -| ↳ `state_id` | number | State the fixture is currently in | -| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | -| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | -| ↳ `starting_at` | string | Start date and time | -| ↳ `result_info` | string | Final result info | -| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Session length in minutes or total laps | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Not used in the Motorsport API | -| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `expectedLineups` | array | Array of expected lineup entries for the player | +| ↳ `id` | number | Unique id of the expected lineup record | +| ↳ `sport_id` | number | Sport of the expected lineup | +| ↳ `fixture_id` | number | Fixture the expected lineup relates to | +| ↳ `player_id` | number | Player in the expected lineup | +| ↳ `team_id` | number | Team of the expected lineup player | +| ↳ `formation_field` | string | Formation field of the player | +| ↳ `position_id` | number | Position id of the player | +| ↳ `detailed_position_id` | number | Detailed position id of the player | +| ↳ `type_id` | number | Type of the expected lineup record | +| ↳ `formation_position` | number | Position of the player in the formation | +| ↳ `player_name` | string | Name of the player | +| ↳ `jersey_number` | number | Jersey number of the player | -### `sportmonks_motorsport_get_fixtures_by_date` +### `sportmonks_football_get_expected_lineups_by_team` -Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks +Retrieve the premium expected lineups for a team ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;venue\) | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fixture\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | -| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixtures` | array | Array of motorsport fixture \(session\) objects for the requested date | -| ↳ `id` | number | Unique id of the fixture \(session\) | -| ↳ `sport_id` | number | Sport of the fixture | -| ↳ `league_id` | number | League the fixture is held in | -| ↳ `season_id` | number | Season the fixture is held in | -| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | -| ↳ `group_id` | number | Not used in the Motorsport API | -| ↳ `aggregate_id` | number | Not used in the Motorsport API | -| ↳ `round_id` | number | Not used in the Motorsport API | -| ↳ `state_id` | number | State the fixture is currently in | -| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | -| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | -| ↳ `starting_at` | string | Start date and time | -| ↳ `result_info` | string | Final result info | -| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Session length in minutes or total laps | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Not used in the Motorsport API | -| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `expectedLineups` | array | Array of expected lineup entries for the team | +| ↳ `id` | number | Unique id of the expected lineup record | +| ↳ `sport_id` | number | Sport of the expected lineup | +| ↳ `fixture_id` | number | Fixture the expected lineup relates to | +| ↳ `player_id` | number | Player in the expected lineup | +| ↳ `team_id` | number | Team of the expected lineup player | +| ↳ `formation_field` | string | Formation field of the player | +| ↳ `position_id` | number | Position id of the player | +| ↳ `detailed_position_id` | number | Detailed position id of the player | +| ↳ `type_id` | number | Type of the expected lineup record | +| ↳ `formation_position` | number | Position of the player in the formation | +| ↳ `player_name` | string | Name of the player | +| ↳ `jersey_number` | number | Jersey number of the player | -### `sportmonks_motorsport_get_fixture` +### `sportmonks_football_get_extended_team_squad` -Retrieve a single motorsport fixture (session) by its ID from Sportmonks +Retrieve all squad entries for a team (based on current seasons) by team ID #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results;latestLaps;pitstops\) | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | | `filters` | string | No | Filters to apply | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `fixture` | object | The requested motorsport fixture \(session\) object | -| ↳ `id` | number | Unique id of the fixture \(session\) | -| ↳ `sport_id` | number | Sport of the fixture | -| ↳ `league_id` | number | League the fixture is held in | -| ↳ `season_id` | number | Season the fixture is held in | -| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | -| ↳ `group_id` | number | Not used in the Motorsport API | -| ↳ `aggregate_id` | number | Not used in the Motorsport API | -| ↳ `round_id` | number | Not used in the Motorsport API | -| ↳ `state_id` | number | State the fixture is currently in | -| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | -| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | -| ↳ `starting_at` | string | Start date and time | -| ↳ `result_info` | string | Final result info | -| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | -| ↳ `details` | string | Details about the fixture | -| ↳ `length` | number | Session length in minutes or total laps | -| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | -| ↳ `has_odds` | boolean | Not used in the Motorsport API | -| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | -| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | +| `squad` | array | Array of extended squad entries for the team | +| ↳ `id` | number | Unique id of the squad record | +| ↳ `transfer_id` | number | Transfer id of the squad record | +| ↳ `player_id` | number | Player in the squad | +| ↳ `team_id` | number | Team of the squad | +| ↳ `position_id` | number | Position of the player in the squad | +| ↳ `detailed_position_id` | number | Detailed position of the player in the squad | +| ↳ `jersey_number` | number | Jersey number of the player | +| ↳ `start` | string | Start contract date of the player | +| ↳ `end` | string | End contract date of the player | -### `sportmonks_motorsport_get_drivers` +### `sportmonks_football_get_fixture` -Retrieve all motorsport drivers from Sportmonks +Retrieve a single football fixture by its ID from Sportmonks #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | -| `filters` | string | No | Filters to apply | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | -| `order` | string | No | Order direction \(asc or desc\) | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events;lineups;statistics\) | +| `filters` | string | No | Filters to apply \(e.g. eventTypes:14\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `drivers` | array | Array of driver objects | -| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | -| ↳ `sport_id` | number | Sport of the driver | -| ↳ `country_id` | number | Country of birth of the driver | -| ↳ `nationality_id` | number | Nationality of the driver | -| ↳ `city_id` | number | City of birth of the driver | -| ↳ `position_id` | number | Position of the driver within the team | -| ↳ `detailed_position_id` | number | Not used in the Motorsport API | -| ↳ `type_id` | number | Not used in the Motorsport API | -| ↳ `common_name` | string | Name the driver is known for | -| ↳ `firstname` | string | First name of the driver | -| ↳ `lastname` | string | Last name of the driver | -| ↳ `name` | string | Name of the driver | -| ↳ `display_name` | string | Display name of the driver | -| ↳ `image_path` | string | URL to the driver headshot | -| ↳ `height` | number | Height of the driver in cm | -| ↳ `weight` | number | Weight of the driver in kg | -| ↳ `date_of_birth` | string | Date of birth of the driver | -| ↳ `gender` | string | Gender of the driver | - -### `sportmonks_motorsport_get_driver` - +| `fixture` | object | The requested fixture object | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixtures_by_date` + +Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;league\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects for the requested date | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixtures_by_date_range` + +Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max range is 100 days. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | Start date in YYYY-MM-DD format | +| `endDate` | string | Yes | End date in YYYY-MM-DD format \(max 100 days after start\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects within the requested date range | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixtures_by_date_range_for_team` + +Retrieve fixtures for a team within a date range (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | Start date in YYYY-MM-DD format | +| `endDate` | string | Yes | End date in YYYY-MM-DD format | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects for the team within the date range | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_fixtures_by_ids` + +Retrieve multiple football fixtures by a comma-separated list of IDs (max 50) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `ids` | string | Yes | Comma-separated fixture IDs \(e.g. 18535517,18535518\). Maximum of 50 IDs | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects for the requested IDs | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_grouped_standings_by_round` + +Retrieve the standing table for a round ID grouped by group where applicable from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `roundId` | string | Yes | The unique id of the round | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply \(e.g. standingGroups:246697\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | json | Standings for the round: an array of groups \(each with id, name and a standings array\) when groups exist, otherwise a flat array of standing entries | + +### `sportmonks_football_get_head_to_head` + +Retrieve the head-to-head fixtures between two teams from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `team1` | string | Yes | The id of the first team | +| `team2` | string | Yes | The id of the second team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of head-to-head fixture objects between the two teams | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_inplay_livescores` + +Retrieve all fixtures that are currently being played (in-play) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of in-play fixture objects | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_latest_coaches` + +Retrieve all coaches that have received updates in the past two hours + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `coaches` | array | Array of recently updated coach objects | +| ↳ `id` | number | Unique id of the coach | +| ↳ `player_id` | number | Player related to the coach | +| ↳ `sport_id` | number | Sport of the coach | +| ↳ `country_id` | number | Country of the coach | +| ↳ `nationality_id` | number | Nationality of the coach | +| ↳ `city_id` | number | Birth city of the coach | +| ↳ `common_name` | string | Common name of the coach | +| ↳ `firstname` | string | First name of the coach | +| ↳ `lastname` | string | Last name of the coach | +| ↳ `name` | string | Name of the coach | +| ↳ `display_name` | string | Display name of the coach | +| ↳ `image_path` | string | URL to the coach headshot | +| ↳ `height` | number | Height of the coach in cm | +| ↳ `weight` | number | Weight of the coach in kg | +| ↳ `date_of_birth` | string | Date of birth of the coach | +| ↳ `gender` | string | Gender of the coach | + +### `sportmonks_football_get_latest_fixtures` + +Retrieve all fixtures that have received updates within the last 10 seconds + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of recently updated fixture objects | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_latest_livescores` + +Retrieve all livescores that have received updates within the last 10 seconds + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of recently updated live fixture objects | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_latest_players` + +Retrieve all players that have received updates in the past two hours + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. nationality;position\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `players` | array | Array of recently updated player objects | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | + +### `sportmonks_football_get_latest_totw` + +Retrieve the latest Team of the Week (TOTW) for a league ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;team;player;round\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `totw` | array | Array of the latest Team of the Week entries for the league | +| ↳ `id` | number | Unique id of the TOTW entry | +| ↳ `player_id` | number | Player of the team of the week | +| ↳ `fixture_id` | number | Fixture the TOTW player played in | +| ↳ `round_id` | number | Round the fixture is played at | +| ↳ `team_id` | number | Team the TOTW player played for | +| ↳ `rating` | string | Rating of the TOTW player | +| ↳ `formation_position` | number | Player position in the TOTW formation | +| ↳ `formation` | string | The TOTW's formation | + +### `sportmonks_football_get_latest_transfers` + +Retrieve the latest transfers available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transfers` | array | Array of the latest transfer objects | +| ↳ `id` | number | Unique id of the transfer | +| ↳ `sport_id` | number | Sport of the transfer | +| ↳ `player_id` | number | Player who transferred | +| ↳ `type_id` | number | Type of the transfer | +| ↳ `from_team_id` | number | Team the player transferred from | +| ↳ `to_team_id` | number | Team the player transferred to | +| ↳ `position_id` | number | Position id of the transfer | +| ↳ `detailed_position_id` | number | Detailed position id of the transfer | +| ↳ `date` | string | Date of the transfer | +| ↳ `career_ended` | boolean | Whether the transfer ended the career | +| ↳ `completed` | boolean | Whether the transfer is completed | +| ↳ `amount` | number | Transfer fee amount | + +### `sportmonks_football_get_league` + +Retrieve a single football league by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason;seasons\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `league` | object | The requested league object | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_leagues` + +Retrieve all football leagues available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_leagues_by_country` + +Retrieve all leagues for a country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects for the country | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_leagues_by_date` + +Retrieve all leagues with fixtures on a given date (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The fixture date in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects with fixtures on the requested date | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_leagues_by_team` + +Retrieve all current and historical leagues for a team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of current and historical league objects for the team | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_live_leagues` + +Retrieve all leagues that have fixtures currently being played from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of currently live league objects | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_get_live_probabilities` + +Retrieve all live (in-play) prediction probabilities from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `predictions` | array | Array of live probability prediction objects | +| ↳ `id` | number | Unique id of the live prediction record | +| ↳ `fixture_id` | number | Fixture the prediction belongs to | +| ↳ `period_id` | number | Match period the prediction was recorded in | +| ↳ `minute` | number | Match minute the prediction was generated | +| ↳ `predictions` | json | Home win, away win and draw probabilities as percentages | +| ↳ `type_id` | number | Type of the prediction \(237 for fulltime result\) | + +### `sportmonks_football_get_live_probabilities_by_fixture` + +Retrieve all live (in-play) prediction probabilities for a fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `predictions` | array | Array of live probability prediction objects for the fixture | +| ↳ `id` | number | Unique id of the live prediction record | +| ↳ `fixture_id` | number | Fixture the prediction belongs to | +| ↳ `period_id` | number | Match period the prediction was recorded in | +| ↳ `minute` | number | Match minute the prediction was generated | +| ↳ `predictions` | json | Home win, away win and draw probabilities as percentages | +| ↳ `type_id` | number | Type of the prediction \(237 for fulltime result\) | + +### `sportmonks_football_get_live_standings_by_league` + +Retrieve the live standing table for a league ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply \(e.g. standingGroups:246697\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of live standing entries for the league | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Group related to the standing | +| ↳ `round_id` | number | Round related to the standing | +| ↳ `standing_rule_id` | number | Standing rule related to the standing | +| ↳ `position` | number | Position of the team in the standing | +| ↳ `result` | string | Movement of the team in the standing | +| ↳ `points` | number | Points the team has gathered | + +### `sportmonks_football_get_livescores` + +Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of live fixture objects | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_match_facts` + +Retrieve all available match facts within your Sportmonks subscription (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | +| `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matchFacts` | array | Array of match fact objects | +| ↳ `id` | number | Unique id of the match fact | +| ↳ `sport_id` | number | Sport of the match fact | +| ↳ `fixture_id` | number | Fixture related to the match fact | +| ↳ `type_id` | number | Type of the match fact | +| ↳ `participant` | string | Team the fact relates to \(home or away\) | +| ↳ `basis` | string | Basis of the match fact \(e.g. h2h, overall\) | +| ↳ `data` | json | Match fact data payload \(counts and percentages\) | +| ↳ `natural_language` | string | Human-readable description of the match fact | +| ↳ `category` | string | Category of the match fact | +| ↳ `scope` | string | Scope of the match fact | + +### `sportmonks_football_get_match_facts_by_date_range` + +Retrieve match facts within a date range (YYYY-MM-DD) from Sportmonks (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | Start date in YYYY-MM-DD format | +| `endDate` | string | Yes | End date in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | +| `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matchFacts` | array | Array of match fact objects within the date range | +| ↳ `id` | number | Unique id of the match fact | +| ↳ `sport_id` | number | Sport of the match fact | +| ↳ `fixture_id` | number | Fixture related to the match fact | +| ↳ `type_id` | number | Type of the match fact | +| ↳ `participant` | string | Team the fact relates to \(home or away\) | +| ↳ `basis` | string | Basis of the match fact \(e.g. h2h, overall\) | +| ↳ `data` | json | Match fact data payload \(counts and percentages\) | +| ↳ `natural_language` | string | Human-readable description of the match fact | +| ↳ `category` | string | Category of the match fact | +| ↳ `scope` | string | Scope of the match fact | + +### `sportmonks_football_get_match_facts_by_fixture` + +Retrieve match facts for a fixture ID from Sportmonks (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | +| `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matchFacts` | array | Array of match fact objects for the fixture | +| ↳ `id` | number | Unique id of the match fact | +| ↳ `sport_id` | number | Sport of the match fact | +| ↳ `fixture_id` | number | Fixture related to the match fact | +| ↳ `type_id` | number | Type of the match fact | +| ↳ `participant` | string | Team the fact relates to \(home or away\) | +| ↳ `basis` | string | Basis of the match fact \(e.g. h2h, overall\) | +| ↳ `data` | json | Match fact data payload \(counts and percentages\) | +| ↳ `natural_language` | string | Human-readable description of the match fact | +| ↳ `category` | string | Category of the match fact | +| ↳ `scope` | string | Scope of the match fact | + +### `sportmonks_football_get_match_facts_by_league` + +Retrieve match facts for a league ID from Sportmonks (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | +| `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matchFacts` | array | Array of match fact objects for the league | +| ↳ `id` | number | Unique id of the match fact | +| ↳ `sport_id` | number | Sport of the match fact | +| ↳ `fixture_id` | number | Fixture related to the match fact | +| ↳ `type_id` | number | Type of the match fact | +| ↳ `participant` | string | Team the fact relates to \(home or away\) | +| ↳ `basis` | string | Basis of the match fact \(e.g. h2h, overall\) | +| ↳ `data` | json | Match fact data payload \(counts and percentages\) | +| ↳ `natural_language` | string | Human-readable description of the match fact | +| ↳ `category` | string | Category of the match fact | +| ↳ `scope` | string | Scope of the match fact | + +### `sportmonks_football_get_past_fixtures_by_tv_station` + +Retrieve all past fixtures that were available for a TV station ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `tvStationId` | string | Yes | The unique id of the TV station | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of past fixture objects for the TV station | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_player` + +Retrieve a single football player by their ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `playerId` | string | Yes | The unique id of the player | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team;statistics\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `player` | object | The requested player object | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | + +### `sportmonks_football_get_players_by_country` + +Retrieve all players for a country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. nationality;position\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order players by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `players` | array | Array of player objects for the country | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | + +### `sportmonks_football_get_postmatch_news` + +Retrieve all post-match news articles available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | +| `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order news by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `news` | array | Array of post-match news articles | +| ↳ `id` | number | Unique id of the news article | +| ↳ `fixture_id` | number | Fixture related to the news article | +| ↳ `league_id` | number | League related to the news article | +| ↳ `title` | string | Title of the news article | +| ↳ `type` | string | Type of the news \(prematch or postmatch\) | + +### `sportmonks_football_get_postmatch_news_by_season` + +Retrieve all post-match news articles for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | +| `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order news \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `news` | array | Array of post-match news articles for the season | +| ↳ `id` | number | Unique id of the news article | +| ↳ `fixture_id` | number | Fixture related to the news article | +| ↳ `league_id` | number | League related to the news article | +| ↳ `title` | string | Title of the news article | +| ↳ `type` | string | Type of the news \(prematch or postmatch\) | + +### `sportmonks_football_get_predictability_by_league` + +Retrieve the predictions model performance for a league ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;league\) | +| `filters` | string | No | Filters to apply \(e.g. predictabilityTypes:245\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `predictability` | array | Array of predictability records for the league | +| ↳ `id` | number | Unique id of the predictability record | +| ↳ `league_id` | number | League related to the predictability | +| ↳ `type_id` | number | Type of the predictability | +| ↳ `data` | json | Predictability values per market | + +### `sportmonks_football_get_prematch_news` + +Retrieve all pre-match news articles available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | +| `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order news by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `news` | array | Array of pre-match news articles | +| ↳ `id` | number | Unique id of the news article | +| ↳ `fixture_id` | number | Fixture related to the news article | +| ↳ `league_id` | number | League related to the news article | +| ↳ `title` | string | Title of the news article | +| ↳ `type` | string | Type of the news \(prematch or postmatch\) | + +### `sportmonks_football_get_prematch_news_by_season` + +Retrieve all pre-match news articles for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | +| `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order news \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `news` | array | Array of pre-match news articles for the season | +| ↳ `id` | number | Unique id of the news article | +| ↳ `fixture_id` | number | Fixture related to the news article | +| ↳ `league_id` | number | League related to the news article | +| ↳ `title` | string | Title of the news article | +| ↳ `type` | string | Type of the news \(prematch or postmatch\) | + +### `sportmonks_football_get_prematch_news_upcoming` + +Retrieve all pre-match news articles for upcoming fixtures from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | +| `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order news \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `news` | array | Array of pre-match news articles for upcoming fixtures | +| ↳ `id` | number | Unique id of the news article | +| ↳ `fixture_id` | number | Fixture related to the news article | +| ↳ `league_id` | number | League related to the news article | +| ↳ `title` | string | Title of the news article | +| ↳ `type` | string | Type of the news \(prematch or postmatch\) | + +### `sportmonks_football_get_probabilities` + +Retrieve all prediction probabilities available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | +| `filters` | string | No | Filters to apply \(e.g. predictionTypes:236\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `predictions` | array | Array of prediction probability objects | +| ↳ `id` | number | Unique id of the prediction | +| ↳ `fixture_id` | number | Fixture related to the prediction | +| ↳ `predictions` | json | Prediction payload \(varies by type: score map, value bet object, etc.\) | +| ↳ `type_id` | number | Type of the prediction | + +### `sportmonks_football_get_probabilities_by_fixture` + +Retrieve prediction probabilities for a fixture by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | +| `filters` | string | No | Filters to apply \(e.g. predictionTypes:236\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `predictions` | array | Array of prediction probability entries for the fixture | +| ↳ `id` | number | Unique id of the prediction | +| ↳ `fixture_id` | number | Fixture related to the prediction | +| ↳ `predictions` | json | Prediction payload \(varies by type: score map, value bet object, etc.\) | +| ↳ `type_id` | number | Type of the prediction | + +### `sportmonks_football_get_referee` + +Retrieve a single football referee by their ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `refereeId` | string | Yes | The unique id of the referee | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;statistics\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `referee` | object | The requested referee object | +| ↳ `id` | number | Unique id of the referee | +| ↳ `sport_id` | number | Sport of the referee | +| ↳ `country_id` | number | Country of the referee | +| ↳ `nationality_id` | number | Nationality of the referee | +| ↳ `city_id` | number | Birth city of the referee | +| ↳ `common_name` | string | Common name of the referee | +| ↳ `firstname` | string | First name of the referee | +| ↳ `lastname` | string | Last name of the referee | +| ↳ `name` | string | Name of the referee | +| ↳ `display_name` | string | Display name of the referee | +| ↳ `image_path` | string | URL to the referee headshot | +| ↳ `height` | number | Height of the referee in cm | +| ↳ `weight` | number | Weight of the referee in kg | +| ↳ `date_of_birth` | string | Date of birth of the referee | +| ↳ `gender` | string | Gender of the referee | + +### `sportmonks_football_get_referees` + +Retrieve all football referees available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;statistics\) | +| `filters` | string | No | Filters to apply \(e.g. refereeCountries:44\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `referees` | array | Array of referee objects | +| ↳ `id` | number | Unique id of the referee | +| ↳ `sport_id` | number | Sport of the referee | +| ↳ `country_id` | number | Country of the referee | +| ↳ `nationality_id` | number | Nationality of the referee | +| ↳ `city_id` | number | Birth city of the referee | +| ↳ `common_name` | string | Common name of the referee | +| ↳ `firstname` | string | First name of the referee | +| ↳ `lastname` | string | Last name of the referee | +| ↳ `name` | string | Name of the referee | +| ↳ `display_name` | string | Display name of the referee | +| ↳ `image_path` | string | URL to the referee headshot | +| ↳ `height` | number | Height of the referee in cm | +| ↳ `weight` | number | Weight of the referee in kg | +| ↳ `date_of_birth` | string | Date of birth of the referee | +| ↳ `gender` | string | Gender of the referee | + +### `sportmonks_football_get_referees_by_country` + +Retrieve all referees for a country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `referees` | array | Array of referee objects for the country | +| ↳ `id` | number | Unique id of the referee | +| ↳ `sport_id` | number | Sport of the referee | +| ↳ `country_id` | number | Country of the referee | +| ↳ `nationality_id` | number | Nationality of the referee | +| ↳ `city_id` | number | Birth city of the referee | +| ↳ `common_name` | string | Common name of the referee | +| ↳ `firstname` | string | First name of the referee | +| ↳ `lastname` | string | Last name of the referee | +| ↳ `name` | string | Name of the referee | +| ↳ `display_name` | string | Display name of the referee | +| ↳ `image_path` | string | URL to the referee headshot | +| ↳ `height` | number | Height of the referee in cm | +| ↳ `weight` | number | Weight of the referee in kg | +| ↳ `date_of_birth` | string | Date of birth of the referee | +| ↳ `gender` | string | Gender of the referee | + +### `sportmonks_football_get_referees_by_season` + +Retrieve all referees for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `referees` | array | Array of referee objects for the season | +| ↳ `id` | number | Unique id of the referee | +| ↳ `sport_id` | number | Sport of the referee | +| ↳ `country_id` | number | Country of the referee | +| ↳ `nationality_id` | number | Nationality of the referee | +| ↳ `city_id` | number | Birth city of the referee | +| ↳ `common_name` | string | Common name of the referee | +| ↳ `firstname` | string | First name of the referee | +| ↳ `lastname` | string | Last name of the referee | +| ↳ `name` | string | Name of the referee | +| ↳ `display_name` | string | Display name of the referee | +| ↳ `image_path` | string | URL to the referee headshot | +| ↳ `height` | number | Height of the referee in cm | +| ↳ `weight` | number | Weight of the referee in kg | +| ↳ `date_of_birth` | string | Date of birth of the referee | +| ↳ `gender` | string | Gender of the referee | + +### `sportmonks_football_get_rivals_by_team` + +Retrieve rival teams for a team by team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team;rival\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rivals` | array | Array of rival relationships for the team | +| ↳ `sport_id` | number | Sport of the rival | +| ↳ `team_id` | number | Team the rivalry belongs to | +| ↳ `rival_id` | number | Rival team id | + +### `sportmonks_football_get_round` + +Retrieve a single football round by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `roundId` | string | Yes | The unique id of the round | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;stage;fixtures\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `round` | object | The requested round object | +| ↳ `id` | number | Unique id of the round | +| ↳ `sport_id` | number | Sport of the round | +| ↳ `league_id` | number | League of the round | +| ↳ `season_id` | number | Season of the round | +| ↳ `stage_id` | number | Stage of the round | +| ↳ `name` | string | Name of the round | +| ↳ `finished` | boolean | Whether the round is finished | +| ↳ `is_current` | boolean | Whether the round is the current round | +| ↳ `starting_at` | string | Start date of the round | +| ↳ `ending_at` | string | End date of the round | +| ↳ `games_in_current_week` | boolean | Whether the round has fixtures this week | + +### `sportmonks_football_get_round_statistics` + +Retrieve all available statistics for a round ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `roundId` | string | Yes | The unique id of the round | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant\) | +| `filters` | string | No | Filters to apply \(e.g. seasonstatisticTypes:52,88\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `statistics` | array | Array of statistic entries for the round | +| ↳ `id` | number | Unique id of the statistic record | +| ↳ `model_id` | number | Id of the entity the statistic belongs to | +| ↳ `type_id` | number | Type of the statistic | +| ↳ `relation_id` | number | Related entity id \(e.g. participant\) when applicable | +| ↳ `value` | json | Statistic value payload \(varies by type\) | + +### `sportmonks_football_get_rounds` + +Retrieve all football rounds available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;stage\) | +| `filters` | string | No | Filters to apply \(e.g. roundSeasons:19735\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rounds` | array | Array of round objects | +| ↳ `id` | number | Unique id of the round | +| ↳ `sport_id` | number | Sport of the round | +| ↳ `league_id` | number | League of the round | +| ↳ `season_id` | number | Season of the round | +| ↳ `stage_id` | number | Stage of the round | +| ↳ `name` | string | Name of the round | +| ↳ `finished` | boolean | Whether the round is finished | +| ↳ `is_current` | boolean | Whether the round is the current round | +| ↳ `starting_at` | string | Start date of the round | +| ↳ `ending_at` | string | End date of the round | +| ↳ `games_in_current_week` | boolean | Whether the round has fixtures this week | + +### `sportmonks_football_get_rounds_by_season` + +Retrieve all rounds for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stage\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rounds` | array | Array of round objects for the season | +| ↳ `id` | number | Unique id of the round | +| ↳ `sport_id` | number | Sport of the round | +| ↳ `league_id` | number | League of the round | +| ↳ `season_id` | number | Season of the round | +| ↳ `stage_id` | number | Stage of the round | +| ↳ `name` | string | Name of the round | +| ↳ `finished` | boolean | Whether the round is finished | +| ↳ `is_current` | boolean | Whether the round is the current round | +| ↳ `starting_at` | string | Start date of the round | +| ↳ `ending_at` | string | End date of the round | +| ↳ `games_in_current_week` | boolean | Whether the round has fixtures this week | + +### `sportmonks_football_get_schedules_by_season` + +Retrieve the full schedule (stages, rounds and fixtures) for a season by season ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `schedules` | json | Array of stages, each with nested rounds and their fixtures \(participants, scores\) | + +### `sportmonks_football_get_schedules_by_season_and_team` + +Retrieve the full season schedule for a specific team by season ID and team ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `teamId` | string | Yes | The unique id of the team | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `schedules` | json | Array of stages, each with nested rounds and their fixtures for the team in the season | + +### `sportmonks_football_get_schedules_by_team` + +Retrieve the full schedule (stages, rounds and fixtures) for a team by team ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `schedules` | json | Array of stages, each with nested rounds and their fixtures \(participants, scores\) | + +### `sportmonks_football_get_season` + +Retrieve a single football season by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages;fixtures\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `season` | object | The requested season object | +| ↳ `id` | number | Unique id of the season | +| ↳ `sport_id` | number | Sport of the season | +| ↳ `league_id` | number | League of the season | +| ↳ `tie_breaker_rule_id` | number | Tie-breaker rule of the season | +| ↳ `name` | string | Name of the season \(e.g. 2023/2024\) | +| ↳ `finished` | boolean | Whether the season is finished | +| ↳ `pending` | boolean | Whether the season is pending | +| ↳ `is_current` | boolean | Whether the season is the current season | +| ↳ `standing_method` | string | Standing calculation method | +| ↳ `starting_at` | string | Start date of the season | +| ↳ `ending_at` | string | End date of the season | +| ↳ `standings_recalculated_at` | string | Last standings recalculation time | +| ↳ `games_in_current_week` | boolean | Whether the season has fixtures this week | + +### `sportmonks_football_get_seasons` + +Retrieve all football seasons available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | +| `filters` | string | No | Filters to apply \(e.g. seasonLeagues:501\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `seasons` | array | Array of season objects | +| ↳ `id` | number | Unique id of the season | +| ↳ `sport_id` | number | Sport of the season | +| ↳ `league_id` | number | League of the season | +| ↳ `tie_breaker_rule_id` | number | Tie-breaker rule of the season | +| ↳ `name` | string | Name of the season \(e.g. 2023/2024\) | +| ↳ `finished` | boolean | Whether the season is finished | +| ↳ `pending` | boolean | Whether the season is pending | +| ↳ `is_current` | boolean | Whether the season is the current season | +| ↳ `standing_method` | string | Standing calculation method | +| ↳ `starting_at` | string | Start date of the season | +| ↳ `ending_at` | string | End date of the season | +| ↳ `standings_recalculated_at` | string | Last standings recalculation time | +| ↳ `games_in_current_week` | boolean | Whether the season has fixtures this week | + +### `sportmonks_football_get_seasons_by_team` + +Retrieve all seasons for a team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `seasons` | array | Array of season objects for the team | +| ↳ `id` | number | Unique id of the season | +| ↳ `sport_id` | number | Sport of the season | +| ↳ `league_id` | number | League of the season | +| ↳ `tie_breaker_rule_id` | number | Tie-breaker rule of the season | +| ↳ `name` | string | Name of the season \(e.g. 2023/2024\) | +| ↳ `finished` | boolean | Whether the season is finished | +| ↳ `pending` | boolean | Whether the season is pending | +| ↳ `is_current` | boolean | Whether the season is the current season | +| ↳ `standing_method` | string | Standing calculation method | +| ↳ `starting_at` | string | Start date of the season | +| ↳ `ending_at` | string | End date of the season | +| ↳ `standings_recalculated_at` | string | Last standings recalculation time | +| ↳ `games_in_current_week` | boolean | Whether the season has fixtures this week | + +### `sportmonks_football_get_stage` + +Retrieve a single football stage by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `stageId` | string | Yes | The unique id of the stage | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;rounds\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stage` | object | The requested stage object | +| ↳ `id` | number | Unique id of the stage | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League of the stage | +| ↳ `season_id` | number | Season of the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Sort order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Start date of the stage | +| ↳ `ending_at` | string | End date of the stage | +| ↳ `games_in_current_week` | boolean | Whether the stage has fixtures this week | + +### `sportmonks_football_get_stage_statistics` + +Retrieve all available statistics for a stage ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `stageId` | string | Yes | The unique id of the stage | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant\) | +| `filters` | string | No | Filters to apply \(e.g. seasonstatisticTypes:52,88\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `statistics` | array | Array of statistic entries for the stage | +| ↳ `id` | number | Unique id of the statistic record | +| ↳ `model_id` | number | Id of the entity the statistic belongs to | +| ↳ `type_id` | number | Type of the statistic | +| ↳ `relation_id` | number | Related entity id \(e.g. participant\) when applicable | +| ↳ `value` | json | Statistic value payload \(varies by type\) | + +### `sportmonks_football_get_stages` + +Retrieve all football stages available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;rounds\) | +| `filters` | string | No | Filters to apply \(e.g. stageSeasons:19735\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stages` | array | Array of stage objects | +| ↳ `id` | number | Unique id of the stage | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League of the stage | +| ↳ `season_id` | number | Season of the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Sort order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Start date of the stage | +| ↳ `ending_at` | string | End date of the stage | +| ↳ `games_in_current_week` | boolean | Whether the stage has fixtures this week | + +### `sportmonks_football_get_stages_by_season` + +Retrieve all stages for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;rounds\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stages` | array | Array of stage objects for the season | +| ↳ `id` | number | Unique id of the stage | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League of the stage | +| ↳ `season_id` | number | Season of the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Sort order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Start date of the stage | +| ↳ `ending_at` | string | End date of the stage | +| ↳ `games_in_current_week` | boolean | Whether the stage has fixtures this week | + +### `sportmonks_football_get_standing_corrections_by_season` + +Retrieve point corrections (awarded or deducted) for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;stage\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `corrections` | array | Array of standing correction entries for the season | +| ↳ `id` | number | Unique id of the standing correction | +| ↳ `season_id` | number | Season related to the correction | +| ↳ `stage_id` | number | Stage related to the correction | +| ↳ `group_id` | number | Group related to the correction | +| ↳ `type_id` | number | Type of the correction | +| ↳ `value` | number | Amount of points awarded or deducted | +| ↳ `calc_type` | string | Calculation type applied \(e.g. + or -\) | +| ↳ `participant_type` | string | Type of the participant \(e.g. team\) | +| ↳ `participant_id` | number | Participant the correction applies to | +| ↳ `active` | boolean | Whether the correction is active | + +### `sportmonks_football_get_standings` + +Retrieve all standings available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;league;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of standing entries | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Group related to the standing | +| ↳ `round_id` | number | Round related to the standing | +| ↳ `standing_rule_id` | number | Standing rule related to the standing | +| ↳ `position` | number | Position of the team in the standing | +| ↳ `result` | string | Movement of the team in the standing | +| ↳ `points` | number | Points the team has gathered | + +### `sportmonks_football_get_standings_by_round` + +Retrieve the full standing table for a round ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `roundId` | string | Yes | The unique id of the round | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply \(e.g. standingGroups:246697\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of standing entries for the round | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Group related to the standing | +| ↳ `round_id` | number | Round related to the standing | +| ↳ `standing_rule_id` | number | Standing rule related to the standing | +| ↳ `position` | number | Position of the team in the standing | +| ↳ `result` | string | Movement of the team in the standing | +| ↳ `points` | number | Points the team has gathered | + +### `sportmonks_football_get_standings_by_season` + +Retrieve the full league standings table for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details;form\) | +| `filters` | string | No | Filters to apply \(e.g. standingStages:77453568\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of standing entries for the season | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Group related to the standing | +| ↳ `round_id` | number | Round related to the standing | +| ↳ `standing_rule_id` | number | Standing rule related to the standing | +| ↳ `position` | number | Position of the team in the standing | +| ↳ `result` | string | Movement of the team in the standing | +| ↳ `points` | number | Points the team has gathered | + +### `sportmonks_football_get_state` + +Retrieve a single fixture state by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `stateId` | string | Yes | The unique id of the state | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `state` | object | The requested fixture state object | +| ↳ `id` | number | Unique id of the state | +| ↳ `state` | string | State code \(e.g. NS, INPLAY_1ST_HALF\) | +| ↳ `name` | string | Full name of the state \(e.g. Not Started\) | +| ↳ `short_name` | string | Short name of the state \(e.g. NS\) | +| ↳ `developer_name` | string | Developer name of the state | + +### `sportmonks_football_get_states` + +Retrieve all fixture states (e.g. Not Started, 1st Half, Full Time) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `states` | array | Array of fixture state objects | +| ↳ `id` | number | Unique id of the state | +| ↳ `state` | string | State code \(e.g. NS, INPLAY_1ST_HALF\) | +| ↳ `name` | string | Full name of the state \(e.g. Not Started\) | +| ↳ `short_name` | string | Short name of the state \(e.g. NS\) | +| ↳ `developer_name` | string | Developer name of the state | + +### `sportmonks_football_get_team` + +Retrieve a single football team by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue;coaches;players.player\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `team` | object | The requested team object | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | + +### `sportmonks_football_get_team_rankings` + +Retrieve all team rankings available within your Sportmonks subscription (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teamRankings` | array | Array of team ranking objects | +| ↳ `id` | number | Unique id of the team ranking | +| ↳ `team_id` | number | Team related to the ranking | +| ↳ `date` | string | Date of the ranking | +| ↳ `current_rank` | number | Placement of the team on that date | +| ↳ `scaled_score` | number | Scaled score of the team \(0-100\) | + +### `sportmonks_football_get_team_rankings_by_date` + +Retrieve team rankings for a given date (YYYY-MM-DD) from Sportmonks (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The ranking date in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teamRankings` | array | Array of team ranking objects for the date | +| ↳ `id` | number | Unique id of the team ranking | +| ↳ `team_id` | number | Team related to the ranking | +| ↳ `date` | string | Date of the ranking | +| ↳ `current_rank` | number | Placement of the team on that date | +| ↳ `scaled_score` | number | Scaled score of the team \(0-100\) | + +### `sportmonks_football_get_team_rankings_by_team` + +Retrieve team rankings for a team ID from Sportmonks (beta) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teamRankings` | array | Array of team ranking objects for the team | +| ↳ `id` | number | Unique id of the team ranking | +| ↳ `team_id` | number | Team related to the ranking | +| ↳ `date` | string | Date of the ranking | +| ↳ `current_rank` | number | Placement of the team on that date | +| ↳ `scaled_score` | number | Scaled score of the team \(0-100\) | + +### `sportmonks_football_get_team_squad` + +Retrieve the current domestic squad for a team by team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `squad` | array | Array of squad entries for the team | +| ↳ `id` | number | Unique id of the squad record | +| ↳ `transfer_id` | number | Transfer id of the squad record | +| ↳ `player_id` | number | Player in the squad | +| ↳ `team_id` | number | Team of the squad | +| ↳ `position_id` | number | Position of the player in the squad | +| ↳ `detailed_position_id` | number | Detailed position of the player in the squad | +| ↳ `jersey_number` | number | Jersey number of the player | +| ↳ `start` | string | Start contract date of the player | +| ↳ `end` | string | End contract date of the player | + +### `sportmonks_football_get_team_squad_by_season` + +Retrieve the (historical) squad for a team in a specific season from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `squad` | array | Array of squad entries for the team in the season | +| ↳ `id` | number | Unique id of the squad record | +| ↳ `transfer_id` | number | Transfer id of the squad record | +| ↳ `player_id` | number | Player in the squad | +| ↳ `team_id` | number | Team of the squad | +| ↳ `position_id` | number | Position of the player in the squad | +| ↳ `detailed_position_id` | number | Detailed position of the player in the squad | +| ↳ `jersey_number` | number | Jersey number of the player | +| ↳ `start` | string | Start contract date of the player | +| ↳ `end` | string | End contract date of the player | + +### `sportmonks_football_get_teams_by_country` + +Retrieve all teams for a country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order teams by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team objects for the country | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | + +### `sportmonks_football_get_teams_by_season` + +Retrieve all teams for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order teams by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team objects for the season | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | + +### `sportmonks_football_get_topscorers_by_season` + +Retrieve the topscorers (goals, assists, cards) for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;participant;type\) | +| `filters` | string | No | Filters to apply \(e.g. seasontopscorerTypes:208\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order topscorers by position \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `topscorers` | array | Array of topscorer entries for the season | +| ↳ `id` | number | Unique id of the topscorer record | +| ↳ `season_id` | number | Season related to the topscorer \(absent on stage topscorers\) | +| ↳ `league_id` | number | League related to the topscorer | +| ↳ `stage_id` | number | Stage related to the topscorer | +| ↳ `player_id` | number | Player related to the topscorer | +| ↳ `participant_id` | number | Team related to the topscorer | +| ↳ `type_id` | number | Type of the topscorer \(goals, assists, cards\) | +| ↳ `position` | number | Position of the topscorer | +| ↳ `total` | number | Number of goals, assists or cards | + +### `sportmonks_football_get_topscorers_by_stage` + +Retrieve topscorers for a stage by stage ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `stageId` | string | Yes | The unique id of the stage | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;participant;type\) | +| `filters` | string | No | Filters to apply \(e.g. stageTopscorerTypes:208\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `topscorers` | array | Array of topscorer entries for the stage | +| ↳ `id` | number | Unique id of the topscorer record | +| ↳ `season_id` | number | Season related to the topscorer \(absent on stage topscorers\) | +| ↳ `league_id` | number | League related to the topscorer | +| ↳ `stage_id` | number | Stage related to the topscorer | +| ↳ `player_id` | number | Player related to the topscorer | +| ↳ `participant_id` | number | Team related to the topscorer | +| ↳ `type_id` | number | Type of the topscorer \(goals, assists, cards\) | +| ↳ `position` | number | Position of the topscorer | +| ↳ `total` | number | Number of goals, assists or cards | + +### `sportmonks_football_get_totw` + +Retrieve all available Team of the Week (TOTW) entries from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;team;player;round\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `totw` | array | Array of Team of the Week entries | +| ↳ `id` | number | Unique id of the TOTW entry | +| ↳ `player_id` | number | Player of the team of the week | +| ↳ `fixture_id` | number | Fixture the TOTW player played in | +| ↳ `round_id` | number | Round the fixture is played at | +| ↳ `team_id` | number | Team the TOTW player played for | +| ↳ `rating` | string | Rating of the TOTW player | +| ↳ `formation_position` | number | Player position in the TOTW formation | +| ↳ `formation` | string | The TOTW's formation | + +### `sportmonks_football_get_totw_by_round` + +Retrieve the Team of the Week (TOTW) for a round ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `roundId` | string | Yes | The unique id of the round | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;team;player;round\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `totw` | array | Array of Team of the Week entries for the round | +| ↳ `id` | number | Unique id of the TOTW entry | +| ↳ `player_id` | number | Player of the team of the week | +| ↳ `fixture_id` | number | Fixture the TOTW player played in | +| ↳ `round_id` | number | Round the fixture is played at | +| ↳ `team_id` | number | Team the TOTW player played for | +| ↳ `rating` | string | Rating of the TOTW player | +| ↳ `formation_position` | number | Player position in the TOTW formation | +| ↳ `formation` | string | The TOTW's formation | + +### `sportmonks_football_get_transfer` + +Retrieve a single transfer by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `transferId` | string | Yes | The unique id of the transfer | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transfer` | object | The requested transfer object | +| ↳ `id` | number | Unique id of the transfer | +| ↳ `sport_id` | number | Sport of the transfer | +| ↳ `player_id` | number | Player who transferred | +| ↳ `type_id` | number | Type of the transfer | +| ↳ `from_team_id` | number | Team the player transferred from | +| ↳ `to_team_id` | number | Team the player transferred to | +| ↳ `position_id` | number | Position id of the transfer | +| ↳ `detailed_position_id` | number | Detailed position id of the transfer | +| ↳ `date` | string | Date of the transfer | +| ↳ `career_ended` | boolean | Whether the transfer ended the career | +| ↳ `completed` | boolean | Whether the transfer is completed | +| ↳ `amount` | number | Transfer fee amount | + +### `sportmonks_football_get_transfer_rumour` + +Retrieve a single transfer rumour by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `rumourId` | string | Yes | The unique id of the transfer rumour | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transferRumour` | object | The requested transfer rumour object | +| ↳ `id` | number | Unique id of the transfer rumour | +| ↳ `sport_id` | number | Sport of the transfer rumour | +| ↳ `player_id` | number | Player the rumour relates to | +| ↳ `position_id` | number | Position id of the player | +| ↳ `from_team_id` | number | Team the player would transfer from | +| ↳ `to_team_id` | number | Team the player would transfer to | +| ↳ `transfer_fee_id` | number | Transfer fee id of the rumour | +| ↳ `probability` | string | Probability of the rumour \(e.g. LOW\) | +| ↳ `source_name` | string | Name of the source of the rumour | +| ↳ `source_url` | string | URL of the source of the rumour | +| ↳ `amount` | number | Estimated transfer fee amount | +| ↳ `currency` | string | Currency of the amount | +| ↳ `date` | string | Date of the rumour | +| ↳ `type_id` | number | Type of the transfer rumour | + +### `sportmonks_football_get_transfer_rumours_between_dates` + +Retrieve transfer rumours within a date range (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | Start date in YYYY-MM-DD format | +| `endDate` | string | Yes | End date in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transferRumours` | array | Array of transfer rumour objects within the date range | +| ↳ `id` | number | Unique id of the transfer rumour | +| ↳ `sport_id` | number | Sport of the transfer rumour | +| ↳ `player_id` | number | Player the rumour relates to | +| ↳ `position_id` | number | Position id of the player | +| ↳ `from_team_id` | number | Team the player would transfer from | +| ↳ `to_team_id` | number | Team the player would transfer to | +| ↳ `transfer_fee_id` | number | Transfer fee id of the rumour | +| ↳ `probability` | string | Probability of the rumour \(e.g. LOW\) | +| ↳ `source_name` | string | Name of the source of the rumour | +| ↳ `source_url` | string | URL of the source of the rumour | +| ↳ `amount` | number | Estimated transfer fee amount | +| ↳ `currency` | string | Currency of the amount | +| ↳ `date` | string | Date of the rumour | +| ↳ `type_id` | number | Type of the transfer rumour | + +### `sportmonks_football_get_transfer_rumours_by_player` + +Retrieve transfer rumours for a player ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `playerId` | string | Yes | The unique id of the player | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transferRumours` | array | Array of transfer rumour objects for the player | +| ↳ `id` | number | Unique id of the transfer rumour | +| ↳ `sport_id` | number | Sport of the transfer rumour | +| ↳ `player_id` | number | Player the rumour relates to | +| ↳ `position_id` | number | Position id of the player | +| ↳ `from_team_id` | number | Team the player would transfer from | +| ↳ `to_team_id` | number | Team the player would transfer to | +| ↳ `transfer_fee_id` | number | Transfer fee id of the rumour | +| ↳ `probability` | string | Probability of the rumour \(e.g. LOW\) | +| ↳ `source_name` | string | Name of the source of the rumour | +| ↳ `source_url` | string | URL of the source of the rumour | +| ↳ `amount` | number | Estimated transfer fee amount | +| ↳ `currency` | string | Currency of the amount | +| ↳ `date` | string | Date of the rumour | +| ↳ `type_id` | number | Type of the transfer rumour | + +### `sportmonks_football_get_transfer_rumours_by_team` + +Retrieve transfer rumours for a team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transferRumours` | array | Array of transfer rumour objects for the team | +| ↳ `id` | number | Unique id of the transfer rumour | +| ↳ `sport_id` | number | Sport of the transfer rumour | +| ↳ `player_id` | number | Player the rumour relates to | +| ↳ `position_id` | number | Position id of the player | +| ↳ `from_team_id` | number | Team the player would transfer from | +| ↳ `to_team_id` | number | Team the player would transfer to | +| ↳ `transfer_fee_id` | number | Transfer fee id of the rumour | +| ↳ `probability` | string | Probability of the rumour \(e.g. LOW\) | +| ↳ `source_name` | string | Name of the source of the rumour | +| ↳ `source_url` | string | URL of the source of the rumour | +| ↳ `amount` | number | Estimated transfer fee amount | +| ↳ `currency` | string | Currency of the amount | +| ↳ `date` | string | Date of the rumour | +| ↳ `type_id` | number | Type of the transfer rumour | + +### `sportmonks_football_get_transfers_between_dates` + +Retrieve transfers within a date range (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | Start date in YYYY-MM-DD format | +| `endDate` | string | Yes | End date in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transfers` | array | Array of transfer objects within the date range | +| ↳ `id` | number | Unique id of the transfer | +| ↳ `sport_id` | number | Sport of the transfer | +| ↳ `player_id` | number | Player who transferred | +| ↳ `type_id` | number | Type of the transfer | +| ↳ `from_team_id` | number | Team the player transferred from | +| ↳ `to_team_id` | number | Team the player transferred to | +| ↳ `position_id` | number | Position id of the transfer | +| ↳ `detailed_position_id` | number | Detailed position id of the transfer | +| ↳ `date` | string | Date of the transfer | +| ↳ `career_ended` | boolean | Whether the transfer ended the career | +| ↳ `completed` | boolean | Whether the transfer is completed | +| ↳ `amount` | number | Transfer fee amount | + +### `sportmonks_football_get_transfers_by_player` + +Retrieve transfers for a player by player ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `playerId` | string | Yes | The unique id of the player | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fromTeam;toTeam;type\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transfers` | array | Array of transfer objects for the player | +| ↳ `id` | number | Unique id of the transfer | +| ↳ `sport_id` | number | Sport of the transfer | +| ↳ `player_id` | number | Player who transferred | +| ↳ `type_id` | number | Type of the transfer | +| ↳ `from_team_id` | number | Team the player transferred from | +| ↳ `to_team_id` | number | Team the player transferred to | +| ↳ `position_id` | number | Position id of the transfer | +| ↳ `detailed_position_id` | number | Detailed position id of the transfer | +| ↳ `date` | string | Date of the transfer | +| ↳ `career_ended` | boolean | Whether the transfer ended the career | +| ↳ `completed` | boolean | Whether the transfer is completed | +| ↳ `amount` | number | Transfer fee amount | + +### `sportmonks_football_get_transfers_by_team` + +Retrieve transfers for a team by team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | +| `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `transfers` | array | Array of transfer objects for the team | +| ↳ `id` | number | Unique id of the transfer | +| ↳ `sport_id` | number | Sport of the transfer | +| ↳ `player_id` | number | Player who transferred | +| ↳ `type_id` | number | Type of the transfer | +| ↳ `from_team_id` | number | Team the player transferred from | +| ↳ `to_team_id` | number | Team the player transferred to | +| ↳ `position_id` | number | Position id of the transfer | +| ↳ `detailed_position_id` | number | Detailed position id of the transfer | +| ↳ `date` | string | Date of the transfer | +| ↳ `career_ended` | boolean | Whether the transfer ended the career | +| ↳ `completed` | boolean | Whether the transfer is completed | +| ↳ `amount` | number | Transfer fee amount | + +### `sportmonks_football_get_tv_station` + +Retrieve a single TV station by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `tvStationId` | string | Yes | The unique id of the TV station | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tvStation` | object | The requested TV station object | +| ↳ `id` | number | Unique id of the TV station | +| ↳ `name` | string | Name of the TV station | +| ↳ `url` | string | URL of the TV station | +| ↳ `image_path` | string | Image path of the TV station | +| ↳ `type` | string | Type of the TV station \(tv, channel\) | +| ↳ `related_id` | number | Related id of the TV station | + +### `sportmonks_football_get_tv_stations` + +Retrieve all TV stations available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tvStations` | array | Array of TV station objects | +| ↳ `id` | number | Unique id of the TV station | +| ↳ `name` | string | Name of the TV station | +| ↳ `url` | string | URL of the TV station | +| ↳ `image_path` | string | Image path of the TV station | +| ↳ `type` | string | Type of the TV station \(tv, channel\) | +| ↳ `related_id` | number | Related id of the TV station | + +### `sportmonks_football_get_tv_stations_by_fixture` + +Retrieve broadcasting TV stations for a fixture by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixtures;countries\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tvStations` | array | Array of TV station objects broadcasting the fixture | +| ↳ `id` | number | Unique id of the TV station | +| ↳ `name` | string | Name of the TV station | +| ↳ `url` | string | URL of the TV station | +| ↳ `image_path` | string | Image path of the TV station | +| ↳ `type` | string | Type of the TV station \(tv, channel\) | +| ↳ `related_id` | number | Related id of the TV station | + +### `sportmonks_football_get_upcoming_fixtures_by_market` + +Retrieve all upcoming fixtures for a market ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `marketId` | string | Yes | The unique id of the market | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;odds\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of upcoming fixture objects for the market | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_upcoming_fixtures_by_tv_station` + +Retrieve all upcoming fixtures available for a TV station ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `tvStationId` | string | Yes | The unique id of the TV station | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of upcoming fixture objects for the TV station | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_get_value_bets` + +Retrieve all value bets available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `valueBets` | array | Array of value bet prediction objects | +| ↳ `id` | number | Unique id of the prediction | +| ↳ `fixture_id` | number | Fixture related to the prediction | +| ↳ `predictions` | json | Prediction payload \(varies by type: score map, value bet object, etc.\) | +| ↳ `type_id` | number | Type of the prediction | + +### `sportmonks_football_get_value_bets_by_fixture` + +Retrieve value bet predictions for a fixture by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `valueBets` | array | Array of value bet prediction entries for the fixture | +| ↳ `id` | number | Unique id of the prediction | +| ↳ `fixture_id` | number | Fixture related to the prediction | +| ↳ `predictions` | json | Prediction payload \(varies by type: score map, value bet object, etc.\) | +| ↳ `type_id` | number | Type of the prediction | + +### `sportmonks_football_get_venue` + +Retrieve a single football venue by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `venueId` | string | Yes | The unique id of the venue | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city;fixtures\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venue` | object | The requested venue object | +| ↳ `id` | number | Unique id of the venue | +| ↳ `country_id` | number | Country of the venue | +| ↳ `city_id` | number | City of the venue | +| ↳ `name` | string | Name of the venue | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Seating capacity of the venue | +| ↳ `image_path` | string | Image path of the venue | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface type of the venue | +| ↳ `national_team` | boolean | Whether the venue is used by the national team | + +### `sportmonks_football_get_venues` + +Retrieve all football venues available within your Sportmonks subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply \(e.g. venueCountries:98\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venues` | array | Array of venue objects | +| ↳ `id` | number | Unique id of the venue | +| ↳ `country_id` | number | Country of the venue | +| ↳ `city_id` | number | City of the venue | +| ↳ `name` | string | Name of the venue | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Seating capacity of the venue | +| ↳ `image_path` | string | Image path of the venue | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface type of the venue | +| ↳ `national_team` | boolean | Whether the venue is used by the national team | + +### `sportmonks_football_get_venues_by_season` + +Retrieve all venues for a season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venues` | array | Array of venue objects for the season | +| ↳ `id` | number | Unique id of the venue | +| ↳ `country_id` | number | Country of the venue | +| ↳ `city_id` | number | City of the venue | +| ↳ `name` | string | Name of the venue | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Seating capacity of the venue | +| ↳ `image_path` | string | Image path of the venue | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface type of the venue | +| ↳ `national_team` | boolean | Whether the venue is used by the national team | + +### `sportmonks_football_search_coaches` + +Search for football coaches by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The coach name to search for \(e.g. Gerrard\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `coaches` | array | Array of coach objects matching the search query | +| ↳ `id` | number | Unique id of the coach | +| ↳ `player_id` | number | Player related to the coach | +| ↳ `sport_id` | number | Sport of the coach | +| ↳ `country_id` | number | Country of the coach | +| ↳ `nationality_id` | number | Nationality of the coach | +| ↳ `city_id` | number | Birth city of the coach | +| ↳ `common_name` | string | Common name of the coach | +| ↳ `firstname` | string | First name of the coach | +| ↳ `lastname` | string | Last name of the coach | +| ↳ `name` | string | Name of the coach | +| ↳ `display_name` | string | Display name of the coach | +| ↳ `image_path` | string | URL to the coach headshot | +| ↳ `height` | number | Height of the coach in cm | +| ↳ `weight` | number | Weight of the coach in kg | +| ↳ `date_of_birth` | string | Date of birth of the coach | +| ↳ `gender` | string | Gender of the coach | + +### `sportmonks_football_search_fixtures` + +Search for football fixtures by name (participants) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The fixture name to search for \(e.g. Celtic\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | +| `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of fixture objects matching the search query | +| ↳ `id` | number | Unique id of the fixture | +| ↳ `sport_id` | number | Sport the fixture is played at | +| ↳ `league_id` | number | League the fixture is played in | +| ↳ `season_id` | number | Season the fixture is played in | +| ↳ `stage_id` | number | Stage the fixture is played in | +| ↳ `group_id` | number | Group the fixture is played in | +| ↳ `aggregate_id` | number | Aggregate the fixture belongs to | +| ↳ `round_id` | number | Round the fixture is played in | +| ↳ `state_id` | number | State \(status\) of the fixture | +| ↳ `venue_id` | number | Venue the fixture is played at | +| ↳ `name` | string | Name of the fixture \(participants\) | +| ↳ `starting_at` | string | Datetime the fixture starts | +| ↳ `result_info` | string | Final result summary | +| ↳ `leg` | string | Leg of the fixture \(e.g. 1/1\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Length of the fixture in minutes | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Whether odds are available | +| ↳ `has_premium_odds` | boolean | Whether premium odds are available | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_football_search_leagues` + +Search for football leagues by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The league name to search for \(e.g. Premier\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order leagues \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects matching the search query | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | number | Whether the league is active \(1\) or inactive \(0\) | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date the last fixture was played | +| ↳ `category` | number | Importance category of the league \(1-4\) | + +### `sportmonks_football_search_players` + +Search for football players by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The player name to search for \(e.g. Tavernier\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order players by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `players` | array | Array of player objects matching the search query | +| ↳ `id` | number | Unique id of the player | +| ↳ `sport_id` | number | Sport of the player | +| ↳ `country_id` | number | Country of birth of the player | +| ↳ `nationality_id` | number | Nationality of the player | +| ↳ `city_id` | number | City of birth of the player | +| ↳ `position_id` | number | Position of the player | +| ↳ `detailed_position_id` | number | Detailed position of the player | +| ↳ `type_id` | number | Type of the player | +| ↳ `common_name` | string | Name the player is known for | +| ↳ `firstname` | string | First name of the player | +| ↳ `lastname` | string | Last name of the player | +| ↳ `name` | string | Name of the player | +| ↳ `display_name` | string | Display name of the player | +| ↳ `image_path` | string | URL to the player headshot | +| ↳ `height` | number | Height of the player in cm | +| ↳ `weight` | number | Weight of the player in kg | +| ↳ `date_of_birth` | string | Date of birth of the player | +| ↳ `gender` | string | Gender of the player | + +### `sportmonks_football_search_referees` + +Search for football referees by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The referee name to search for | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `referees` | array | Array of referee objects matching the search query | +| ↳ `id` | number | Unique id of the referee | +| ↳ `sport_id` | number | Sport of the referee | +| ↳ `country_id` | number | Country of the referee | +| ↳ `nationality_id` | number | Nationality of the referee | +| ↳ `city_id` | number | Birth city of the referee | +| ↳ `common_name` | string | Common name of the referee | +| ↳ `firstname` | string | First name of the referee | +| ↳ `lastname` | string | Last name of the referee | +| ↳ `name` | string | Name of the referee | +| ↳ `display_name` | string | Display name of the referee | +| ↳ `image_path` | string | URL to the referee headshot | +| ↳ `height` | number | Height of the referee in cm | +| ↳ `weight` | number | Weight of the referee in kg | +| ↳ `date_of_birth` | string | Date of birth of the referee | +| ↳ `gender` | string | Gender of the referee | + +### `sportmonks_football_search_rounds` + +Search for football rounds by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The round name to search for \(e.g. 5\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rounds` | array | Array of round objects matching the search query | +| ↳ `id` | number | Unique id of the round | +| ↳ `sport_id` | number | Sport of the round | +| ↳ `league_id` | number | League of the round | +| ↳ `season_id` | number | Season of the round | +| ↳ `stage_id` | number | Stage of the round | +| ↳ `name` | string | Name of the round | +| ↳ `finished` | boolean | Whether the round is finished | +| ↳ `is_current` | boolean | Whether the round is the current round | +| ↳ `starting_at` | string | Start date of the round | +| ↳ `ending_at` | string | End date of the round | +| ↳ `games_in_current_week` | boolean | Whether the round has fixtures this week | + +### `sportmonks_football_search_seasons` + +Search for football seasons by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The season name to search for \(e.g. 2023/2024\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `seasons` | array | Array of season objects matching the search query | +| ↳ `id` | number | Unique id of the season | +| ↳ `sport_id` | number | Sport of the season | +| ↳ `league_id` | number | League of the season | +| ↳ `tie_breaker_rule_id` | number | Tie-breaker rule of the season | +| ↳ `name` | string | Name of the season \(e.g. 2023/2024\) | +| ↳ `finished` | boolean | Whether the season is finished | +| ↳ `pending` | boolean | Whether the season is pending | +| ↳ `is_current` | boolean | Whether the season is the current season | +| ↳ `standing_method` | string | Standing calculation method | +| ↳ `starting_at` | string | Start date of the season | +| ↳ `ending_at` | string | End date of the season | +| ↳ `standings_recalculated_at` | string | Last standings recalculation time | +| ↳ `games_in_current_week` | boolean | Whether the season has fixtures this week | + +### `sportmonks_football_search_stages` + +Search for football stages by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The stage name to search for \(e.g. Group\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stages` | array | Array of stage objects matching the search query | +| ↳ `id` | number | Unique id of the stage | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League of the stage | +| ↳ `season_id` | number | Season of the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Sort order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Start date of the stage | +| ↳ `ending_at` | string | End date of the stage | +| ↳ `games_in_current_week` | boolean | Whether the stage has fixtures this week | + +### `sportmonks_football_search_teams` + +Search for football teams by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The team name to search for \(e.g. Celtic\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order teams by id \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team objects matching the search query | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Home venue of the team | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the last played match | + +### `sportmonks_football_search_venues` + +Search for football venues by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The venue name to search for \(e.g. Celtic Park\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venues` | array | Array of venue objects matching the search query | +| ↳ `id` | number | Unique id of the venue | +| ↳ `country_id` | number | Country of the venue | +| ↳ `city_id` | number | City of the venue | +| ↳ `name` | string | Name of the venue | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Seating capacity of the venue | +| ↳ `image_path` | string | Image path of the venue | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface type of the venue | +| ↳ `national_team` | boolean | Whether the venue is used by the national team | + +### `sportmonks_motorsport_get_all_fixtures` + +Retrieve all motorsport fixtures (sessions) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of motorsport fixture \(session\) objects | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_current_leagues_by_team` + +Retrieve the current motorsport leagues for a team by team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team \(constructor\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of current league objects for the team | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_driver` + Retrieve a single motorsport driver by their ID from Sportmonks #### Input @@ -725,45 +3985,2016 @@ Retrieve a single motorsport driver by their ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `driverId` | string | Yes | The unique id of the driver | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | -| `filters` | string | No | Filters to apply | +| `driverId` | string | Yes | The unique id of the driver | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `driver` | object | The requested driver object | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_driver_standings` + +Retrieve all driver championship standings from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of driver standing entries | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Driver or team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `standing_rule_id` | number | Not used in the Motorsport API | +| ↳ `position` | number | Position of the participant in the standing | +| ↳ `result` | string | Not used in the Motorsport API | +| ↳ `points` | number | Points the participant has gathered | + +### `sportmonks_motorsport_get_driver_standings_by_season` + +Retrieve the drivers championship standings for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of driver standing entries for the season | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Driver or team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `standing_rule_id` | number | Not used in the Motorsport API | +| ↳ `position` | number | Position of the participant in the standing | +| ↳ `result` | string | Not used in the Motorsport API | +| ↳ `points` | number | Points the participant has gathered | + +### `sportmonks_motorsport_get_drivers` + +Retrieve all motorsport drivers from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of driver objects | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_drivers_by_country` + +Retrieve all motorsport drivers for a country by country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of driver objects for the country | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_drivers_by_season` + +Retrieve all motorsport drivers for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of driver objects for the season | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_fixture` + +Retrieve a single motorsport fixture (session) by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results;latestLaps;pitstops\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixture` | object | The requested motorsport fixture \(session\) object | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_fixtures_by_date` + +Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of motorsport fixture \(session\) objects for the requested date | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_fixtures_by_date_range` + +Retrieve motorsport fixtures (sessions) between two dates (YYYY-MM-DD, max 100 days) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `startDate` | string | Yes | The start date of the range, in YYYY-MM-DD format | +| `endDate` | string | Yes | The end date of the range, in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;venue\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order fixtures by starting_at \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of motorsport fixture \(session\) objects within the requested date range | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_fixtures_by_ids` + +Retrieve multiple motorsport fixtures (sessions) by their IDs (max 50) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureIds` | string | Yes | Comma-separated list of fixture ids \(max 50, e.g. 19408487,19408480\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of motorsport fixture \(session\) objects for the requested ids | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_laps_by_fixture` + +Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `laps` | array | Array of lap objects for the fixture | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_laps_by_fixture_and_driver` + +Retrieve all laps for a motorsport fixture and driver from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `driverId` | string | Yes | The unique id of the driver | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `laps` | array | Array of lap objects for the fixture and driver | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_laps_by_fixture_and_lap` + +Retrieve all laps for a motorsport fixture and lap number from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `lapNumber` | string | Yes | The lap number to retrieve | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `laps` | array | Array of lap objects for the fixture and lap number | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_latest_laps_by_fixture` + +Retrieve the latest laps for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `laps` | array | Array of the latest lap objects for the fixture | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_latest_pitstops_by_fixture` + +Retrieve the latest pitstops for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pitstops` | array | Array of the latest pitstop objects for the fixture | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_latest_stints_by_fixture` + +Retrieve the latest tyre stints for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stints` | array | Array of the latest stint objects for the fixture | +| ↳ `id` | number | Unique id of the stint | +| ↳ `fixture_id` | number | Fixture related to the stint | +| ↳ `stint_number` | number | Stint number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the stint | +| ↳ `is_latest` | boolean | Whether it is the latest stint | + +### `sportmonks_motorsport_get_latest_updated_drivers` + +Retrieve the most recently updated motorsport drivers from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of recently updated driver objects | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_get_latest_updated_fixtures` + +Retrieve the most recently updated motorsport fixtures (sessions) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of recently updated motorsport fixture \(session\) objects | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_league` + +Retrieve a single motorsport league by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `leagueId` | string | Yes | The unique id of the league | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `league` | object | The requested league object | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_leagues` + +Retrieve all motorsport leagues from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_leagues_by_country` + +Retrieve all motorsport leagues for a country by country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects for the country | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_leagues_by_date` + +Retrieve all motorsport leagues with fixtures on a specific date (YYYY-MM-DD) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `date` | string | Yes | The date to fetch leagues for, in YYYY-MM-DD format | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects with fixtures on the requested date | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_leagues_by_live` + +Retrieve all motorsport leagues that currently have live fixtures from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects that currently have live fixtures | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_leagues_by_team` + +Retrieve all current and historical motorsport leagues for a team by team ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team \(constructor\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects for the team | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_livescores` + +Retrieve all live motorsport fixtures (sessions) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fixtures` | array | Array of live motorsport fixture \(session\) objects | +| ↳ `id` | number | Unique id of the fixture \(session\) | +| ↳ `sport_id` | number | Sport of the fixture | +| ↳ `league_id` | number | League the fixture is held in | +| ↳ `season_id` | number | Season the fixture is held in | +| ↳ `stage_id` | number | Stage \(race weekend\) the fixture is held in | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `aggregate_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `state_id` | number | State the fixture is currently in | +| ↳ `venue_id` | number | Venue \(track\) the fixture is held at | +| ↳ `name` | string | Name of the fixture \(e.g. Practice 1, Race\) | +| ↳ `starting_at` | string | Start date and time | +| ↳ `result_info` | string | Final result info | +| ↳ `leg` | string | Stage of the fixture \(e.g. 2/3 for Practice 2\) | +| ↳ `details` | string | Details about the fixture | +| ↳ `length` | number | Session length in minutes or total laps | +| ↳ `placeholder` | boolean | Whether the fixture is a placeholder | +| ↳ `has_odds` | boolean | Not used in the Motorsport API | +| ↳ `has_premium_odds` | boolean | Not used in the Motorsport API | +| ↳ `starting_at_timestamp` | number | UNIX timestamp of the start time | + +### `sportmonks_motorsport_get_pitstops_by_fixture` + +Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pitstops` | array | Array of pitstop objects for the fixture | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_pitstops_by_fixture_and_driver` + +Retrieve all pitstops for a motorsport fixture and driver from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `driverId` | string | Yes | The unique id of the driver | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pitstops` | array | Array of pitstop objects for the fixture and driver | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_pitstops_by_fixture_and_lap` + +Retrieve all pitstops for a motorsport fixture and lap number from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `lapNumber` | string | Yes | The lap number to retrieve | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pitstops` | array | Array of pitstop objects for the fixture and lap number | +| ↳ `id` | number | Unique id of the lap/pitstop | +| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | +| ↳ `lap_number` | number | Lap number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the lap/pitstop | +| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | + +### `sportmonks_motorsport_get_race_results_by_season_and_driver` + +Retrieve race results (stages with fixtures, lineups and lineup details) for a season and driver from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `driverId` | string | Yes | The unique id of the driver | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Array of stage objects for the season and driver, each including nested fixtures, lineups and lineup details | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_race_results_by_season_and_team` + +Retrieve race results (stages with fixtures, lineups and lineup details) for a season and team from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `teamId` | string | Yes | The unique id of the team \(constructor\) | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Array of stage objects for the season and team, each including nested fixtures, lineups and lineup details | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_schedules_by_season` + +Retrieve the full schedule (stages with nested fixtures and venues) for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `schedules` | array | Array of stage objects for the season schedule, each including nested fixtures and venues | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_season` + +Retrieve a single motorsport season by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `season` | object | The requested season object | +| ↳ `id` | number | Unique id of the season | +| ↳ `sport_id` | number | Sport of the season | +| ↳ `league_id` | number | League of the season | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | +| ↳ `name` | string | Name of the season | +| ↳ `finished` | boolean | Whether the season is finished | +| ↳ `pending` | boolean | Whether the season is pending | +| ↳ `is_current` | boolean | Whether the season is the current season | +| ↳ `starting_at` | string | Starting date of the season | +| ↳ `ending_at` | string | Ending date of the season | +| ↳ `standings_recalculated_at` | string | Timestamp when standings were last updated | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_seasons` + +Retrieve all motorsport seasons from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `seasons` | array | Array of season objects | +| ↳ `id` | number | Unique id of the season | +| ↳ `sport_id` | number | Sport of the season | +| ↳ `league_id` | number | League of the season | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | +| ↳ `name` | string | Name of the season | +| ↳ `finished` | boolean | Whether the season is finished | +| ↳ `pending` | boolean | Whether the season is pending | +| ↳ `is_current` | boolean | Whether the season is the current season | +| ↳ `starting_at` | string | Starting date of the season | +| ↳ `ending_at` | string | Ending date of the season | +| ↳ `standings_recalculated_at` | string | Timestamp when standings were last updated | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_stage` + +Retrieve a single motorsport stage (race weekend) by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `stageId` | string | Yes | The unique id of the stage \(race weekend\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stage` | object | The requested stage \(race weekend\) object | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_stages` + +Retrieve all motorsport stages (race weekends) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stages` | array | Array of stage \(race weekend\) objects | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_stages_by_season` + +Retrieve all motorsport stages (race weekends) for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stages` | array | Array of stage \(race weekend\) objects for the season | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_state` + +Retrieve a single motorsport fixture state by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `stateId` | string | Yes | The unique id of the state | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `state` | object | The requested fixture state object | +| ↳ `id` | number | Unique id of the state | +| ↳ `state` | string | Abbreviation of the state | +| ↳ `name` | string | Full name of the state | +| ↳ `short_name` | string | Short name of the state | +| ↳ `developer_name` | string | Name recommended for developers to use | + +### `sportmonks_motorsport_get_states` + +Retrieve all possible motorsport fixture states from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `states` | array | Array of fixture state objects | +| ↳ `id` | number | Unique id of the state | +| ↳ `state` | string | Abbreviation of the state | +| ↳ `name` | string | Full name of the state | +| ↳ `short_name` | string | Short name of the state | +| ↳ `developer_name` | string | Name recommended for developers to use | + +### `sportmonks_motorsport_get_stints_by_fixture` + +Retrieve all tyre stints for a motorsport fixture (session) by fixture ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stints` | array | Array of stint objects for the fixture | +| ↳ `id` | number | Unique id of the stint | +| ↳ `fixture_id` | number | Fixture related to the stint | +| ↳ `stint_number` | number | Stint number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the stint | +| ↳ `is_latest` | boolean | Whether it is the latest stint | + +### `sportmonks_motorsport_get_stints_by_fixture_and_driver` + +Retrieve all tyre stints for a motorsport fixture and driver from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `driverId` | string | Yes | The unique id of the driver | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stints` | array | Array of stint objects for the fixture and driver | +| ↳ `id` | number | Unique id of the stint | +| ↳ `fixture_id` | number | Fixture related to the stint | +| ↳ `stint_number` | number | Stint number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the stint | +| ↳ `is_latest` | boolean | Whether it is the latest stint | + +### `sportmonks_motorsport_get_stints_by_fixture_and_stint` + +Retrieve all tyre stints for a motorsport fixture and stint number from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | +| `stintNumber` | string | Yes | The stint number to retrieve | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stints` | array | Array of stint objects for the fixture and stint number | +| ↳ `id` | number | Unique id of the stint | +| ↳ `fixture_id` | number | Fixture related to the stint | +| ↳ `stint_number` | number | Stint number in the fixture | +| ↳ `driver_number` | number | Number of the driver | +| ↳ `participant_id` | number | Driver related to the stint | +| ↳ `is_latest` | boolean | Whether it is the latest stint | + +### `sportmonks_motorsport_get_team` + +Retrieve a single motorsport team (constructor) by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `teamId` | string | Yes | The unique id of the team \(constructor\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `team` | object | The requested team \(constructor\) object | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_get_team_standings` + +Retrieve all team (constructor) championship standings from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of team \(constructor\) standing entries | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Driver or team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `standing_rule_id` | number | Not used in the Motorsport API | +| ↳ `position` | number | Position of the participant in the standing | +| ↳ `result` | string | Not used in the Motorsport API | +| ↳ `points` | number | Points the participant has gathered | + +### `sportmonks_motorsport_get_team_standings_by_season` + +Retrieve the constructors championship standings for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `standings` | array | Array of team \(constructor\) standing entries for the season | +| ↳ `id` | number | Unique id of the standing | +| ↳ `participant_id` | number | Driver or team related to the standing | +| ↳ `sport_id` | number | Sport related to the standing | +| ↳ `league_id` | number | League related to the standing | +| ↳ `season_id` | number | Season related to the standing | +| ↳ `stage_id` | number | Stage related to the standing | +| ↳ `group_id` | number | Not used in the Motorsport API | +| ↳ `round_id` | number | Not used in the Motorsport API | +| ↳ `standing_rule_id` | number | Not used in the Motorsport API | +| ↳ `position` | number | Position of the participant in the standing | +| ↳ `result` | string | Not used in the Motorsport API | +| ↳ `points` | number | Points the participant has gathered | + +### `sportmonks_motorsport_get_teams` + +Retrieve all motorsport teams (constructors) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team \(constructor\) objects | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_get_teams_by_country` + +Retrieve all motorsport teams (constructors) for a country by country ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `countryId` | string | Yes | The unique id of the country | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team \(constructor\) objects for the country | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_get_teams_by_season` + +Retrieve all motorsport teams (constructors) for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team \(constructor\) objects for the season | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_get_venue` + +Retrieve a single motorsport venue (racing track) by its ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `venueId` | string | Yes | The unique id of the venue \(track\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venue` | object | The requested venue \(racing track\) object | +| ↳ `id` | number | Unique id of the venue \(track\) | +| ↳ `country_id` | number | Country the venue is in | +| ↳ `city_id` | number | City the venue is in | +| ↳ `name` | string | Name of the venue/track | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Capacity of the venue | +| ↳ `image_path` | string | URL to the track layout image | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface of the venue | +| ↳ `national_team` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_venues` + +Retrieve all motorsport venues (racing tracks) from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venues` | array | Array of venue \(racing track\) objects | +| ↳ `id` | number | Unique id of the venue \(track\) | +| ↳ `country_id` | number | Country the venue is in | +| ↳ `city_id` | number | City the venue is in | +| ↳ `name` | string | Name of the venue/track | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Capacity of the venue | +| ↳ `image_path` | string | URL to the track layout image | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface of the venue | +| ↳ `national_team` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_get_venues_by_season` + +Retrieve all motorsport venues (racing tracks) for a season by season ID from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `seasonId` | string | Yes | The unique id of the season | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venues` | array | Array of venue \(racing track\) objects for the season | +| ↳ `id` | number | Unique id of the venue \(track\) | +| ↳ `country_id` | number | Country the venue is in | +| ↳ `city_id` | number | City the venue is in | +| ↳ `name` | string | Name of the venue/track | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Capacity of the venue | +| ↳ `image_path` | string | URL to the track layout image | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface of the venue | +| ↳ `national_team` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_search_drivers` + +Search for motorsport drivers by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The driver name to search for \(e.g. Verstappen\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `drivers` | array | Array of driver objects matching the search query | +| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | +| ↳ `sport_id` | number | Sport of the driver | +| ↳ `country_id` | number | Country of birth of the driver | +| ↳ `nationality_id` | number | Nationality of the driver | +| ↳ `city_id` | number | City of birth of the driver | +| ↳ `position_id` | number | Position of the driver within the team | +| ↳ `detailed_position_id` | number | Not used in the Motorsport API | +| ↳ `type_id` | number | Not used in the Motorsport API | +| ↳ `common_name` | string | Name the driver is known for | +| ↳ `firstname` | string | First name of the driver | +| ↳ `lastname` | string | Last name of the driver | +| ↳ `name` | string | Name of the driver | +| ↳ `display_name` | string | Display name of the driver | +| ↳ `image_path` | string | URL to the driver headshot | +| ↳ `height` | number | Height of the driver in cm | +| ↳ `weight` | number | Weight of the driver in kg | +| ↳ `date_of_birth` | string | Date of birth of the driver | +| ↳ `gender` | string | Gender of the driver | + +### `sportmonks_motorsport_search_leagues` + +Search for motorsport leagues by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The league name to search for \(e.g. Formula\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leagues` | array | Array of league objects matching the search query | +| ↳ `id` | number | Unique id of the league | +| ↳ `sport_id` | number | Sport of the league | +| ↳ `country_id` | number | Country of the league | +| ↳ `name` | string | Name of the league | +| ↳ `active` | boolean | Whether the league is active | +| ↳ `short_code` | string | Short code of the league | +| ↳ `image_path` | string | URL to the league logo | +| ↳ `type` | string | Type of the league | +| ↳ `sub_type` | string | Subtype of the league | +| ↳ `last_played_at` | string | Date of the last fixture held in the league | +| ↳ `category` | number | Category of the league | +| ↳ `has_jerseys` | boolean | Not used in the Motorsport API | + +### `sportmonks_motorsport_search_stages` + +Search for motorsport stages (race weekends) by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The stage name to search for \(e.g. Monaco\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stages` | array | Array of stage \(race weekend\) objects matching the search query | +| ↳ `id` | number | Unique id of the stage \(race weekend\) | +| ↳ `sport_id` | number | Sport of the stage | +| ↳ `league_id` | number | League related to the stage | +| ↳ `season_id` | number | Season related to the stage | +| ↳ `type_id` | number | Type of the stage | +| ↳ `name` | string | Name of the stage | +| ↳ `sort_order` | number | Order of the stage | +| ↳ `finished` | boolean | Whether the stage is finished | +| ↳ `is_current` | boolean | Whether the stage is the current stage | +| ↳ `starting_at` | string | Starting date of the stage | +| ↳ `ending_at` | string | Ending date of the stage | +| ↳ `games_in_current_week` | boolean | Not used in the Motorsport API | +| ↳ `tie_breaker_rule_id` | number | Not used in the Motorsport API | + +### `sportmonks_motorsport_search_teams` + +Search for motorsport teams (constructors) by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The team name to search for \(e.g. Bull\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of team \(constructor\) objects matching the search query | +| ↳ `id` | number | Unique id of the team | +| ↳ `sport_id` | number | Sport of the team | +| ↳ `country_id` | number | Country of the team | +| ↳ `venue_id` | number | Not used in the Motorsport API | +| ↳ `gender` | string | Gender of the team | +| ↳ `name` | string | Name of the team \(constructor\) | +| ↳ `short_code` | string | Short code of the team | +| ↳ `image_path` | string | URL to the team logo | +| ↳ `founded` | number | Founding year of the team | +| ↳ `type` | string | Type of the team | +| ↳ `placeholder` | boolean | Whether the team is a placeholder | +| ↳ `last_played_at` | string | Date and time of the team's last session | + +### `sportmonks_motorsport_search_venues` + +Search for motorsport venues (racing tracks) by name from Sportmonks + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The venue name to search for \(e.g. Hungaroring\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `venues` | array | Array of venue \(racing track\) objects matching the search query | +| ↳ `id` | number | Unique id of the venue \(track\) | +| ↳ `country_id` | number | Country the venue is in | +| ↳ `city_id` | number | City the venue is in | +| ↳ `name` | string | Name of the venue/track | +| ↳ `address` | string | Address of the venue | +| ↳ `zipcode` | string | Zipcode of the venue | +| ↳ `latitude` | string | Latitude of the venue | +| ↳ `longitude` | string | Longitude of the venue | +| ↳ `capacity` | number | Capacity of the venue | +| ↳ `image_path` | string | URL to the track layout image | +| ↳ `city_name` | string | Name of the city the venue is in | +| ↳ `surface` | string | Surface of the venue | +| ↳ `national_team` | boolean | Not used in the Motorsport API | + +### `sportmonks_odds_get_all_historical_odds` + +Retrieve all available historical (premium) pre-match odd values from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. odd\) | +| `filters` | string | No | Filters to apply \(e.g. winningOdds\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `historicalOdds` | array | Array of historical premium odd value records | +| ↳ `id` | number | Unique id of the history record | +| ↳ `odd_id` | number | Premium odd this history record belongs to | +| ↳ `value` | string | Historical decimal odds value | +| ↳ `probability` | string | Implied probability at this point in time | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `bookmaker_update` | string | Bookmaker's update timestamp for this record \(UTC\) | + +### `sportmonks_odds_get_all_inplay_odds` + +Retrieve all available live (in-play) odds from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12, bookmakers:2,14, IdAfter:oddID\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of in-play odd objects | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `external_id` | number | External id of the odd | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `suspended` | boolean | Whether the odd is suspended | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_all_pre_match_odds` + +Retrieve all available pre-match odds from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12, bookmakers:2,14, winningOdds, IdAfter:oddID\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of pre-match odd objects | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | +| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `original_label` | string | Original handicap value of the odd \(handicap markets\) | + +### `sportmonks_odds_get_all_premium_odds` + +Retrieve all available premium (historical) pre-match odds from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12, bookmakers:2,14, IdAfter:oddID\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `premiumOdds` | array | Array of premium odd objects | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 29.85%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `created_at` | string | Timestamp the odd was created \(UTC\) | +| ↳ `updated_at` | string | Timestamp the odd was last updated \(UTC\) | +| ↳ `latest_bookmaker_update` | string | Bookmaker's own last-update timestamp \(UTC\) | + +### `sportmonks_odds_get_bookmaker` + +Retrieve a single bookmaker by its ID from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `bookmakerId` | string | Yes | The unique id of the bookmaker | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmaker` | object | The requested bookmaker object | +| ↳ `id` | number | Unique id of the bookmaker | +| ↳ `name` | string | Name of the bookmaker | +| ↳ `logo` | string | Logo of the bookmaker | + +### `sportmonks_odds_get_bookmaker_event_ids_by_fixture` + +Retrieve bookmakers' own event ids mapped to a Sportmonks fixture via the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `driver` | object | The requested driver object | -| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | -| ↳ `sport_id` | number | Sport of the driver | -| ↳ `country_id` | number | Country of birth of the driver | -| ↳ `nationality_id` | number | Nationality of the driver | -| ↳ `city_id` | number | City of birth of the driver | -| ↳ `position_id` | number | Position of the driver within the team | -| ↳ `detailed_position_id` | number | Not used in the Motorsport API | -| ↳ `type_id` | number | Not used in the Motorsport API | -| ↳ `common_name` | string | Name the driver is known for | -| ↳ `firstname` | string | First name of the driver | -| ↳ `lastname` | string | Last name of the driver | -| ↳ `name` | string | Name of the driver | -| ↳ `display_name` | string | Display name of the driver | -| ↳ `image_path` | string | URL to the driver headshot | -| ↳ `height` | number | Height of the driver in cm | -| ↳ `weight` | number | Weight of the driver in kg | -| ↳ `date_of_birth` | string | Date of birth of the driver | -| ↳ `gender` | string | Gender of the driver | +| `bookmakerEvents` | array | Array of bookmaker event mapping records for the fixture | +| ↳ `fixture_id` | number | Sportmonks fixture id | +| ↳ `bookmaker_id` | number | Id of the bookmaker | +| ↳ `bookmaker_name` | string | Name of the bookmaker | +| ↳ `bookmaker_event_id` | string | The fixture's event id at the bookmaker | -### `sportmonks_motorsport_search_drivers` +### `sportmonks_odds_get_bookmakers` -Search for motorsport drivers by name from Sportmonks +Retrieve all bookmakers from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `query` | string | Yes | The driver name to search for \(e.g. Verstappen\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | +| `filters` | string | No | Filters to apply \(e.g. IdAfter:bookmakerID\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -772,102 +6003,286 @@ Search for motorsport drivers by name from Sportmonks | Parameter | Type | Description | | --------- | ---- | ----------- | -| `drivers` | array | Array of driver objects matching the search query | -| ↳ `id` | number | Unique id of the driver \(player_id in responses\) | -| ↳ `sport_id` | number | Sport of the driver | -| ↳ `country_id` | number | Country of birth of the driver | -| ↳ `nationality_id` | number | Nationality of the driver | -| ↳ `city_id` | number | City of birth of the driver | -| ↳ `position_id` | number | Position of the driver within the team | -| ↳ `detailed_position_id` | number | Not used in the Motorsport API | -| ↳ `type_id` | number | Not used in the Motorsport API | -| ↳ `common_name` | string | Name the driver is known for | -| ↳ `firstname` | string | First name of the driver | -| ↳ `lastname` | string | Last name of the driver | -| ↳ `name` | string | Name of the driver | -| ↳ `display_name` | string | Display name of the driver | -| ↳ `image_path` | string | URL to the driver headshot | -| ↳ `height` | number | Height of the driver in cm | -| ↳ `weight` | number | Weight of the driver in kg | -| ↳ `date_of_birth` | string | Date of birth of the driver | -| ↳ `gender` | string | Gender of the driver | +| `bookmakers` | array | Array of bookmaker objects | +| ↳ `id` | number | Unique id of the bookmaker | +| ↳ `name` | string | Name of the bookmaker | +| ↳ `logo` | string | Logo of the bookmaker | + +### `sportmonks_odds_get_bookmakers_by_fixture` + +Retrieve all bookmakers available for a fixture from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bookmakers` | array | Array of bookmaker objects available for the fixture | +| ↳ `id` | number | Unique id of the bookmaker | +| ↳ `name` | string | Name of the bookmaker | +| ↳ `logo` | string | Logo of the bookmaker | + +### `sportmonks_odds_get_inplay_odds_by_fixture` + +Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of in-play odd objects for the fixture | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `external_id` | number | External id of the odd | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `suspended` | boolean | Whether the odd is suspended | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker` + +Retrieve live (in-play) odds for a fixture from a specific bookmaker via the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `bookmakerId` | string | Yes | The unique id of the bookmaker | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of in-play odd objects for the fixture and bookmaker | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `external_id` | number | External id of the odd | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `suspended` | boolean | Whether the odd is suspended | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_inplay_odds_by_fixture_and_market` + +Retrieve live (in-play) odds for a fixture on a specific market via the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `marketId` | string | Yes | The unique id of the market | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. bookmakers:2,14\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of in-play odd objects for the fixture and market | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `external_id` | number | External id of the odd | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `suspended` | boolean | Whether the odd is suspended | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_last_updated_inplay_odds` + +Retrieve in-play odds updated in the last 10 seconds from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of in-play odd objects updated in the last 10 seconds | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `external_id` | number | External id of the odd | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `suspended` | boolean | Whether the odd is suspended | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | + +### `sportmonks_odds_get_last_updated_pre_match_odds` + +Retrieve pre-match odds updated in the last 10 seconds from the Sportmonks Odds API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `odds` | array | Array of pre-match odd objects updated in the last 10 seconds | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | +| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `original_label` | string | Original handicap value of the odd \(handicap markets\) | -### `sportmonks_motorsport_get_teams` +### `sportmonks_odds_get_market` -Retrieve all motorsport teams (constructors) from Sportmonks +Retrieve a single betting market by its ID from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | -| `filters` | string | No | Filters to apply | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | -| `order` | string | No | Order direction \(asc or desc\) | +| `marketId` | string | Yes | The unique id of the market | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `teams` | array | Array of team \(constructor\) objects | -| ↳ `id` | number | Unique id of the team | -| ↳ `sport_id` | number | Sport of the team | -| ↳ `country_id` | number | Country of the team | -| ↳ `venue_id` | number | Not used in the Motorsport API | -| ↳ `gender` | string | Gender of the team | -| ↳ `name` | string | Name of the team \(constructor\) | -| ↳ `short_code` | string | Short code of the team | -| ↳ `image_path` | string | URL to the team logo | -| ↳ `founded` | number | Founding year of the team | -| ↳ `type` | string | Type of the team | -| ↳ `placeholder` | boolean | Whether the team is a placeholder | -| ↳ `last_played_at` | string | Date and time of the team's last session | +| `market` | object | The requested market object | +| ↳ `id` | number | Unique id of the market | +| ↳ `name` | string | Name of the market | +| ↳ `developer_name` | string | Developer \(machine-readable\) name of the market | -### `sportmonks_motorsport_get_team` +### `sportmonks_odds_get_markets` -Retrieve a single motorsport team (constructor) by its ID from Sportmonks +Retrieve all betting markets from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `teamId` | string | Yes | The unique id of the team \(constructor\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | -| `filters` | string | No | Filters to apply | +| `filters` | string | No | Filters to apply \(e.g. IdAfter:marketID\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `team` | object | The requested team \(constructor\) object | -| ↳ `id` | number | Unique id of the team | -| ↳ `sport_id` | number | Sport of the team | -| ↳ `country_id` | number | Country of the team | -| ↳ `venue_id` | number | Not used in the Motorsport API | -| ↳ `gender` | string | Gender of the team | -| ↳ `name` | string | Name of the team \(constructor\) | -| ↳ `short_code` | string | Short code of the team | -| ↳ `image_path` | string | URL to the team logo | -| ↳ `founded` | number | Founding year of the team | -| ↳ `type` | string | Type of the team | -| ↳ `placeholder` | boolean | Whether the team is a placeholder | -| ↳ `last_played_at` | string | Date and time of the team's last session | +| `markets` | array | Array of market objects | +| ↳ `id` | number | Unique id of the market | +| ↳ `name` | string | Name of the market | +| ↳ `developer_name` | string | Developer \(machine-readable\) name of the market | -### `sportmonks_motorsport_get_driver_standings_by_season` +### `sportmonks_odds_get_pre_match_odds_by_fixture` -Retrieve the drivers championship standings for a season by season ID from Sportmonks +Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `seasonId` | string | Yes | The unique id of the season | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | -| `filters` | string | No | Filters to apply | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -876,107 +6291,146 @@ Retrieve the drivers championship standings for a season by season ID from Sport | Parameter | Type | Description | | --------- | ---- | ----------- | -| `standings` | array | Array of driver standing entries for the season | -| ↳ `id` | number | Unique id of the standing | -| ↳ `participant_id` | number | Driver or team related to the standing | -| ↳ `sport_id` | number | Sport related to the standing | -| ↳ `league_id` | number | League related to the standing | -| ↳ `season_id` | number | Season related to the standing | -| ↳ `stage_id` | number | Stage related to the standing | -| ↳ `group_id` | number | Not used in the Motorsport API | -| ↳ `round_id` | number | Not used in the Motorsport API | -| ↳ `standing_rule_id` | number | Not used in the Motorsport API | -| ↳ `position` | number | Position of the participant in the standing | -| ↳ `result` | string | Not used in the Motorsport API | -| ↳ `points` | number | Points the participant has gathered | +| `odds` | array | Array of pre-match odd objects for the fixture | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | +| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `original_label` | string | Original handicap value of the odd \(handicap markets\) | -### `sportmonks_motorsport_get_team_standings_by_season` +### `sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker` -Retrieve the constructors championship standings for a season by season ID from Sportmonks +Retrieve pre-match odds for a fixture from a specific bookmaker via the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `seasonId` | string | Yes | The unique id of the season | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | -| `filters` | string | No | Filters to apply | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | -| `order` | string | No | Order direction \(asc or desc\) | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `bookmakerId` | string | Yes | The unique id of the bookmaker | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or winningOdds\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `standings` | array | Array of team \(constructor\) standing entries for the season | -| ↳ `id` | number | Unique id of the standing | -| ↳ `participant_id` | number | Driver or team related to the standing | -| ↳ `sport_id` | number | Sport related to the standing | -| ↳ `league_id` | number | League related to the standing | -| ↳ `season_id` | number | Season related to the standing | -| ↳ `stage_id` | number | Stage related to the standing | -| ↳ `group_id` | number | Not used in the Motorsport API | -| ↳ `round_id` | number | Not used in the Motorsport API | -| ↳ `standing_rule_id` | number | Not used in the Motorsport API | -| ↳ `position` | number | Position of the participant in the standing | -| ↳ `result` | string | Not used in the Motorsport API | -| ↳ `points` | number | Points the participant has gathered | +| `odds` | array | Array of pre-match odd objects for the fixture and bookmaker | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | +| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `original_label` | string | Original handicap value of the odd \(handicap markets\) | -### `sportmonks_motorsport_get_laps_by_fixture` +### `sportmonks_odds_get_pre_match_odds_by_fixture_and_market` -Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmonks +Retrieve pre-match odds for a fixture on a specific market via the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | -| `filters` | string | No | Filters to apply | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `marketId` | string | Yes | The unique id of the market | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. bookmakers:2,14 or winningOdds\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `laps` | array | Array of lap objects for the fixture | -| ↳ `id` | number | Unique id of the lap/pitstop | -| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | -| ↳ `lap_number` | number | Lap number in the fixture | -| ↳ `driver_number` | number | Number of the driver | -| ↳ `participant_id` | number | Driver related to the lap/pitstop | -| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | +| `odds` | array | Array of pre-match odd objects for the fixture and market | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | +| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | +| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `original_label` | string | Original handicap value of the odd \(handicap markets\) | -### `sportmonks_motorsport_get_pitstops_by_fixture` +### `sportmonks_odds_get_premium_odds_by_fixture` -Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Sportmonks +Retrieve premium (historical) pre-match odds for a fixture from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `fixtureId` | string | Yes | The unique id of the fixture \(session\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | -| `filters` | string | No | Filters to apply | +| `fixtureId` | string | Yes | The unique id of the fixture | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `pitstops` | array | Array of pitstop objects for the fixture | -| ↳ `id` | number | Unique id of the lap/pitstop | -| ↳ `fixture_id` | number | Fixture related to the lap/pitstop | -| ↳ `lap_number` | number | Lap number in the fixture | -| ↳ `driver_number` | number | Number of the driver | -| ↳ `participant_id` | number | Driver related to the lap/pitstop | -| ↳ `is_latest` | boolean | Whether it is the latest lap/pitstop | +| `premiumOdds` | array | Array of premium odd objects for the fixture | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 29.85%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `created_at` | string | Timestamp the odd was created \(UTC\) | +| ↳ `updated_at` | string | Timestamp the odd was last updated \(UTC\) | +| ↳ `latest_bookmaker_update` | string | Bookmaker's own last-update timestamp \(UTC\) | -### `sportmonks_odds_get_pre_match_odds_by_fixture` +### `sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker` -Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API +Retrieve premium pre-match odds for a fixture from a specific bookmaker via the Sportmonks Odds API #### Input @@ -984,39 +6438,38 @@ Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | +| `bookmakerId` | string | Yes | The unique id of the bookmaker | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | -| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | -| `order` | string | No | Order direction \(asc or desc\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `odds` | array | Array of pre-match odd objects for the fixture | +| `premiumOdds` | array | Array of premium odd objects for the fixture and bookmaker | | ↳ `id` | number | Unique id of the odd | | ↳ `fixture_id` | number | Fixture the odd belongs to | | ↳ `market_id` | number | Market the odd belongs to | | ↳ `bookmaker_id` | number | Bookmaker offering the odd | -| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `label` | string | Outcome label | | ↳ `value` | string | Decimal odds value | -| ↳ `name` | string | Outcome name \(e.g. Home, Draw, Away\) | +| ↳ `name` | string | Outcome name | | ↳ `sort_order` | number | Sort order of the odd | | ↳ `market_description` | string | Description of the market | -| ↳ `probability` | string | Implied probability \(e.g. 48.78%\) | +| ↳ `probability` | string | Implied probability \(e.g. 29.85%\) | | ↳ `dp3` | string | Decimal odds to 3 decimal places | -| ↳ `fractional` | string | Fractional odds \(e.g. 31/15\) | -| ↳ `american` | string | American/moneyline odds \(e.g. +104\) | -| ↳ `winning` | boolean | Whether this is the winning outcome | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | | ↳ `stopped` | boolean | Whether the odd is stopped | | ↳ `total` | string | Total line for over/under markets | | ↳ `handicap` | string | Handicap line for handicap markets | -| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `created_at` | string | Timestamp the odd was created \(UTC\) | +| ↳ `updated_at` | string | Timestamp the odd was last updated \(UTC\) | +| ↳ `latest_bookmaker_update` | string | Bookmaker's own last-update timestamp \(UTC\) | -### `sportmonks_odds_get_inplay_odds_by_fixture` +### `sportmonks_odds_get_premium_odds_by_fixture_and_market` -Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API +Retrieve premium pre-match odds for a fixture on a specific market via the Sportmonks Odds API #### Input @@ -1024,48 +6477,48 @@ Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odd | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | +| `marketId` | string | Yes | The unique id of the market | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | -| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | -| `order` | string | No | Order direction \(asc or desc\) | +| `filters` | string | No | Filters to apply \(e.g. bookmakers:2,14\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `odds` | array | Array of in-play odd objects for the fixture | +| `premiumOdds` | array | Array of premium odd objects for the fixture and market | | ↳ `id` | number | Unique id of the odd | | ↳ `fixture_id` | number | Fixture the odd belongs to | -| ↳ `external_id` | number | External id of the odd | | ↳ `market_id` | number | Market the odd belongs to | | ↳ `bookmaker_id` | number | Bookmaker offering the odd | -| ↳ `label` | string | Outcome label \(e.g. 1, X, 2\) | +| ↳ `label` | string | Outcome label | | ↳ `value` | string | Decimal odds value | | ↳ `name` | string | Outcome name | | ↳ `sort_order` | number | Sort order of the odd | | ↳ `market_description` | string | Description of the market | -| ↳ `probability` | string | Implied probability | +| ↳ `probability` | string | Implied probability \(e.g. 29.85%\) | | ↳ `dp3` | string | Decimal odds to 3 decimal places | | ↳ `fractional` | string | Fractional odds | | ↳ `american` | string | American/moneyline odds | -| ↳ `winning` | boolean | Whether this is the winning outcome | -| ↳ `suspended` | boolean | Whether the odd is suspended | | ↳ `stopped` | boolean | Whether the odd is stopped | | ↳ `total` | string | Total line for over/under markets | | ↳ `handicap` | string | Handicap line for handicap markets | -| ↳ `participants` | string | Participant ids related to the outcome | +| ↳ `created_at` | string | Timestamp the odd was created \(UTC\) | +| ↳ `updated_at` | string | Timestamp the odd was last updated \(UTC\) | +| ↳ `latest_bookmaker_update` | string | Bookmaker's own last-update timestamp \(UTC\) | -### `sportmonks_odds_get_bookmakers` +### `sportmonks_odds_get_updated_historical_odds_between` -Retrieve all bookmakers from the Sportmonks Odds API +Retrieve historical (premium) odds updated between two UNIX timestamps (max 5 minutes) from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `filters` | string | No | Filters to apply \(e.g. IdAfter:bookmakerID\) | +| `fromTimestamp` | string | Yes | Start of the range as a UNIX timestamp \(e.g. 1767225600\) | +| `toTimestamp` | string | Yes | End of the range as a UNIX timestamp \(max 5 minutes after the start\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. odd\) | +| `filters` | string | No | Filters to apply \(e.g. winningOdds\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -1074,30 +6527,57 @@ Retrieve all bookmakers from the Sportmonks Odds API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `bookmakers` | array | Array of bookmaker objects | -| ↳ `id` | number | Unique id of the bookmaker | -| ↳ `name` | string | Name of the bookmaker | -| ↳ `logo` | string | Logo of the bookmaker | +| `historicalOdds` | array | Array of historical premium odd value records updated within the time range | +| ↳ `id` | number | Unique id of the history record | +| ↳ `odd_id` | number | Premium odd this history record belongs to | +| ↳ `value` | string | Historical decimal odds value | +| ↳ `probability` | string | Implied probability at this point in time | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `bookmaker_update` | string | Bookmaker's update timestamp for this record \(UTC\) | -### `sportmonks_odds_get_bookmaker` +### `sportmonks_odds_get_updated_premium_odds_between` -Retrieve a single bookmaker by its ID from the Sportmonks Odds API +Retrieve premium odds updated between two UNIX timestamps (max 5 minutes) from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `bookmakerId` | string | Yes | The unique id of the bookmaker | +| `fromTimestamp` | string | Yes | Start of the range as a UNIX timestamp \(e.g. 1767225600\) | +| `toTimestamp` | string | Yes | End of the range as a UNIX timestamp \(max 5 minutes after the start\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | +| `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `bookmaker` | object | The requested bookmaker object | -| ↳ `id` | number | Unique id of the bookmaker | -| ↳ `name` | string | Name of the bookmaker | -| ↳ `logo` | string | Logo of the bookmaker | +| `premiumOdds` | array | Array of premium odd objects updated within the time range | +| ↳ `id` | number | Unique id of the odd | +| ↳ `fixture_id` | number | Fixture the odd belongs to | +| ↳ `market_id` | number | Market the odd belongs to | +| ↳ `bookmaker_id` | number | Bookmaker offering the odd | +| ↳ `label` | string | Outcome label | +| ↳ `value` | string | Decimal odds value | +| ↳ `name` | string | Outcome name | +| ↳ `sort_order` | number | Sort order of the odd | +| ↳ `market_description` | string | Description of the market | +| ↳ `probability` | string | Implied probability \(e.g. 29.85%\) | +| ↳ `dp3` | string | Decimal odds to 3 decimal places | +| ↳ `fractional` | string | Fractional odds | +| ↳ `american` | string | American/moneyline odds | +| ↳ `stopped` | boolean | Whether the odd is stopped | +| ↳ `total` | string | Total line for over/under markets | +| ↳ `handicap` | string | Handicap line for handicap markets | +| ↳ `created_at` | string | Timestamp the odd was created \(UTC\) | +| ↳ `updated_at` | string | Timestamp the odd was last updated \(UTC\) | +| ↳ `latest_bookmaker_update` | string | Bookmaker's own last-update timestamp \(UTC\) | ### `sportmonks_odds_search_bookmakers` @@ -1111,6 +6591,7 @@ Search for bookmakers by name from the Sportmonks Odds API | `query` | string | Yes | The bookmaker name to search for \(e.g. bet365\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output @@ -1121,16 +6602,16 @@ Search for bookmakers by name from the Sportmonks Odds API | ↳ `name` | string | Name of the bookmaker | | ↳ `logo` | string | Logo of the bookmaker | -### `sportmonks_odds_get_markets` +### `sportmonks_odds_search_markets` -Retrieve all betting markets from the Sportmonks Odds API +Search for betting markets by name from the Sportmonks Odds API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `filters` | string | No | Filters to apply \(e.g. IdAfter:marketID\) | +| `query` | string | Yes | The market name to search for \(e.g. Over/Under\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -1139,91 +6620,104 @@ Retrieve all betting markets from the Sportmonks Odds API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `markets` | array | Array of market objects | +| `markets` | array | Array of market objects matching the search query | | ↳ `id` | number | Unique id of the market | | ↳ `name` | string | Name of the market | +| ↳ `developer_name` | string | Developer \(machine-readable\) name of the market | -### `sportmonks_odds_get_market` +### `sportmonks_core_get_cities` -Retrieve a single betting market by its ID from the Sportmonks Odds API +Retrieve all cities from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `marketId` | string | Yes | The unique id of the market | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `market` | object | The requested market object | -| ↳ `id` | number | Unique id of the market | -| ↳ `name` | string | Name of the market | +| `cities` | array | Array of city objects | +| ↳ `id` | number | Unique id of the city | +| ↳ `country_id` | number | Country of the city | +| ↳ `region_id` | number | Region id of the city | +| ↳ `name` | string | Name of the city | +| ↳ `latitude` | string | Latitude of the city | +| ↳ `longitude` | string | Longitude of the city | +| ↳ `geonameid` | number | Official geonameid of the city | -### `sportmonks_odds_search_markets` +### `sportmonks_core_get_city` -Search for betting markets by name from the Sportmonks Odds API +Retrieve a single city by its ID from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `query` | string | Yes | The market name to search for \(e.g. Over/Under\) | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | +| `cityId` | string | Yes | The unique id of the city | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `markets` | array | Array of market objects matching the search query | -| ↳ `id` | number | Unique id of the market | -| ↳ `name` | string | Name of the market | +| `city` | object | The requested city object | +| ↳ `id` | number | Unique id of the city | +| ↳ `country_id` | number | Country of the city | +| ↳ `region_id` | number | Region id of the city | +| ↳ `name` | string | Name of the city | +| ↳ `latitude` | string | Latitude of the city | +| ↳ `longitude` | string | Longitude of the city | +| ↳ `geonameid` | number | Official geonameid of the city | -### `sportmonks_core_get_continents` +### `sportmonks_core_get_continent` -Retrieve all continents from the Sportmonks Core API +Retrieve a single continent by its ID from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | +| `continentId` | string | Yes | The unique id of the continent | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. countries\) | -| `filters` | string | No | Filters to apply | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | -| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `continents` | array | Array of continent objects | +| `continent` | object | The requested continent object | | ↳ `id` | number | Unique id of the continent | | ↳ `name` | string | Name of the continent | | ↳ `code` | string | Short code of the continent | -### `sportmonks_core_get_continent` +### `sportmonks_core_get_continents` -Retrieve a single continent by its ID from the Sportmonks Core API +Retrieve all continents from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `continentId` | string | Yes | The unique id of the continent | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. countries\) | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `continent` | object | The requested continent object | +| `continents` | array | Array of continent objects | | ↳ `id` | number | Unique id of the continent | | ↳ `name` | string | Name of the continent | | ↳ `code` | string | Short code of the continent | @@ -1291,49 +6785,31 @@ Retrieve a single country by its ID from the Sportmonks Core API | ↳ `borders` | array | Neighbouring countries \(ISO3 codes\) | | ↳ `image_path` | string | Image path to the country flag | -### `sportmonks_core_search_countries` +### `sportmonks_core_get_entity_filters` -Search for countries by name from the Sportmonks Core API +Retrieve all available filters grouped per entity from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `query` | string | Yes | The country name to search for \(e.g. Brazil\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent\) | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `countries` | array | Array of country objects matching the search query | -| ↳ `id` | number | Unique id of the country | -| ↳ `continent_id` | number | Continent of the country | -| ↳ `name` | string | Name of the country | -| ↳ `official_name` | string | Official name of the country | -| ↳ `fifa_name` | string | Official FIFA short code name | -| ↳ `iso2` | string | Two letter country code | -| ↳ `iso3` | string | Three letter country code | -| ↳ `latitude` | string | Latitude position of the country | -| ↳ `longitude` | string | Longitude position of the country | -| ↳ `geonameid` | number | Official geonameid | -| ↳ `borders` | array | Neighbouring countries \(ISO3 codes\) | -| ↳ `image_path` | string | Image path to the country flag | +| `entityFilters` | json | Map of entity name to its available filter names, e.g. \{fixture: \["fixtureLeagues", "fixtureSeasons"\], event: \["eventTypes"\]\} | -### `sportmonks_core_get_regions` +### `sportmonks_core_get_my_usage` -Retrieve all regions from the Sportmonks Core API +Retrieve your Sportmonks API usage aggregated per 5 minutes #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | -| `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -1342,10 +6818,14 @@ Retrieve all regions from the Sportmonks Core API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `regions` | array | Array of region objects | -| ↳ `id` | number | Unique id of the region | -| ↳ `country_id` | number | Country of the region | -| ↳ `name` | string | Name of the region | +| `usage` | array | Array of API usage records aggregated per 5-minute period | +| ↳ `id` | number | Identifier of the usage record | +| ↳ `endpoint` | string | Identifier of the requested endpoint | +| ↳ `count` | number | Total calls for the given timeframe | +| ↳ `entity` | string | The entity the rate limit applies on | +| ↳ `remaining_requests` | number | Amount of requests remaining for the entity in the hourly rate limit | +| ↳ `period_start` | number | Timestamp representing the aggregation start time | +| ↳ `period_end` | number | Timestamp representing the aggregation end time | ### `sportmonks_core_get_region` @@ -1368,16 +6848,16 @@ Retrieve a single region by its ID from the Sportmonks Core API | ↳ `country_id` | number | Country of the region | | ↳ `name` | string | Name of the region | -### `sportmonks_core_get_cities` +### `sportmonks_core_get_regions` -Retrieve all cities from the Sportmonks Core API +Retrieve all regions from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -1387,66 +6867,66 @@ Retrieve all cities from the Sportmonks Core API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `cities` | array | Array of city objects | -| ↳ `id` | number | Unique id of the city | -| ↳ `country_id` | number | Country of the city | -| ↳ `region` | number | Region of the city | -| ↳ `name` | string | Name of the city | -| ↳ `latitude` | string | Latitude of the city | -| ↳ `longitude` | string | Longitude of the city | -| ↳ `geonameid` | number | Official geonameid of the city | +| `regions` | array | Array of region objects | +| ↳ `id` | number | Unique id of the region | +| ↳ `country_id` | number | Country of the region | +| ↳ `name` | string | Name of the region | -### `sportmonks_core_get_city` +### `sportmonks_core_get_timezones` -Retrieve a single city by its ID from the Sportmonks Core API +Retrieve all supported time zones (IANA names) from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `cityId` | string | Yes | The unique id of the city | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `city` | object | The requested city object | -| ↳ `id` | number | Unique id of the city | -| ↳ `country_id` | number | Country of the city | -| ↳ `region` | number | Region of the city | -| ↳ `name` | string | Name of the city | -| ↳ `latitude` | string | Latitude of the city | -| ↳ `longitude` | string | Longitude of the city | -| ↳ `geonameid` | number | Official geonameid of the city | +| `timezones` | array | Array of supported IANA time zone names \(e.g. Europe/London\) | -### `sportmonks_core_search_cities` +### `sportmonks_core_get_type` -Search for cities by name from the Sportmonks Core API +Retrieve a single type by its ID from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `query` | string | Yes | The city name to search for \(e.g. London\) | -| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | -| `per_page` | string | No | Number of results per page \(max 50, default 25\) | -| `page` | string | No | Page number to retrieve | +| `typeId` | string | Yes | The unique id of the type | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `cities` | array | Array of city objects matching the search query | -| ↳ `id` | number | Unique id of the city | -| ↳ `country_id` | number | Country of the city | -| ↳ `region` | number | Region of the city | -| ↳ `name` | string | Name of the city | -| ↳ `latitude` | string | Latitude of the city | -| ↳ `longitude` | string | Longitude of the city | -| ↳ `geonameid` | number | Official geonameid of the city | +| `type` | object | The requested type object | +| ↳ `id` | number | Unique id of the type | +| ↳ `parent_id` | number | Parent type of the type | +| ↳ `name` | string | Name of the type | +| ↳ `code` | string | Code of the type | +| ↳ `developer_name` | string | Developer name of the type | +| ↳ `group` | string | Group the type falls under | +| ↳ `description` | string | Description of the type | + +### `sportmonks_core_get_type_by_entity` + +Retrieve the available types grouped per entity from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `typesByEntity` | json | Map of entity name to its available types, e.g. \{CoachStatisticDetail: \{updated_at, types: \[\{id, name, code, developer_name, model_type, stat_group\}\]\}\} | ### `sportmonks_core_get_types` @@ -1474,44 +6954,92 @@ Retrieve all types (reference data describing events, statistics, positions, etc | ↳ `group` | string | Group the type falls under | | ↳ `description` | string | Description of the type | -### `sportmonks_core_get_type` +### `sportmonks_core_search_cities` -Retrieve a single type by its ID from the Sportmonks Core API +Search for cities by name from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | -| `typeId` | string | Yes | The unique id of the type | +| `query` | string | Yes | The city name to search for \(e.g. London\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `type` | object | The requested type object | -| ↳ `id` | number | Unique id of the type | -| ↳ `parent_id` | number | Parent type of the type | -| ↳ `name` | string | Name of the type | -| ↳ `code` | string | Code of the type | -| ↳ `developer_name` | string | Developer name of the type | -| ↳ `group` | string | Group the type falls under | -| ↳ `description` | string | Description of the type | +| `cities` | array | Array of city objects matching the search query | +| ↳ `id` | number | Unique id of the city | +| ↳ `country_id` | number | Country of the city | +| ↳ `region_id` | number | Region id of the city | +| ↳ `name` | string | Name of the city | +| ↳ `latitude` | string | Latitude of the city | +| ↳ `longitude` | string | Longitude of the city | +| ↳ `geonameid` | number | Official geonameid of the city | -### `sportmonks_core_get_timezones` +### `sportmonks_core_search_countries` -Retrieve all supported time zones (IANA names) from the Sportmonks Core API +Search for countries by name from the Sportmonks Core API #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The country name to search for \(e.g. Brazil\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent;regions\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `timezones` | array | Array of supported IANA time zone names \(e.g. Europe/London\) | +| `countries` | array | Array of country objects matching the search query | +| ↳ `id` | number | Unique id of the country | +| ↳ `continent_id` | number | Continent of the country | +| ↳ `name` | string | Name of the country | +| ↳ `official_name` | string | Official name of the country | +| ↳ `fifa_name` | string | Official FIFA short code name | +| ↳ `iso2` | string | Two letter country code | +| ↳ `iso3` | string | Three letter country code | +| ↳ `latitude` | string | Latitude position of the country | +| ↳ `longitude` | string | Longitude position of the country | +| ↳ `geonameid` | number | Official geonameid | +| ↳ `borders` | array | Neighbouring countries \(ISO3 codes\) | +| ↳ `image_path` | string | Image path to the country flag | + +### `sportmonks_core_search_regions` + +Search for regions by name from the Sportmonks Core API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Sportmonks API token | +| `query` | string | Yes | The region name to search for \(e.g. Utrecht\) | +| `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | +| `filters` | string | No | Filters to apply | +| `per_page` | string | No | Number of results per page \(max 50, default 25\) | +| `page` | string | No | Page number to retrieve | +| `order` | string | No | Order direction \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `regions` | array | Array of region objects matching the search query | +| ↳ `id` | number | Unique id of the region | +| ↳ `country_id` | number | Country of the region | +| ↳ `name` | string | Name of the region | diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index a88a39160b6..4ac5bd3533f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -32,7 +32,10 @@ interface AgentGroupProps { items: AgentGroupItem[] isDelegating?: boolean isStreaming?: boolean - defaultExpanded?: boolean + /** This group is the latest section in its parent sequence (drives collapse). */ + isCurrentSection?: boolean + /** The subagent lane is still open (no subagent_end yet) — i.e. actively running. */ + isLaneOpen?: boolean } export function isAgentGroupResolved(items: AgentGroupItem[]): boolean { @@ -55,7 +58,8 @@ export function AgentGroup({ items, isDelegating = false, isStreaming = false, - defaultExpanded = false, + isCurrentSection = false, + isLaneOpen = false, }: AgentGroupProps) { const AgentIcon = getAgentIcon(agentName) const hasItems = items.length > 0 @@ -66,11 +70,17 @@ export function AgentGroup({ // transport gating is needed to stop an aborted-before-first-tool spinner. const showDelegatingSpinner = isDelegating && !resolved - // Expand only while the turn is live and the group is still open or working. - // Once the turn ends (isStreaming false) — or a subagent closes mid-turn — the - // group auto-collapses, so finished subagent blocks never stay expanded. A - // manual toggle pins the choice for the rest of the message. - const autoExpanded = isStreaming && (defaultExpanded || !resolved) + // Expand while the turn is live and any of: the lane is open (the subagent is + // actively running), this is the current/latest section, or there is unresolved + // work. A finished group stays open until the NEXT section starts (it is no + // longer the latest), instead of collapsing the instant its own work resolves. + // Keying "still running" off the lane-open signal (not `resolved` alone) avoids + // a collapse/reopen flicker on parallel siblings: a subagent's tools all + // momentarily read "done" in the gap between its last search and its `respond` + // ("Gathering thoughts") tool, transiently flipping `resolved` true; the open + // lane bridges that gap so the row never collapses mid-run. The turn ending + // (isStreaming false) collapses everything; a manual toggle pins the choice. + const autoExpanded = isStreaming && (isCurrentSection || isLaneOpen || !resolved) const [manualExpanded, setManualExpanded] = useState(null) const expanded = manualExpanded ?? autoExpanded @@ -135,7 +145,8 @@ export function AgentGroup({ items={item.group.items} isDelegating={item.group.isDelegating} isStreaming={isStreaming} - defaultExpanded={item.group.isOpen} + isCurrentSection={idx === items.length - 1} + isLaneOpen={item.group.isOpen} />
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts index 4a9a1a2ddf0..f211a5f42e5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts @@ -3,4 +3,3 @@ export { AgentGroup, CircleStop, isAgentGroupResolved } from './agent-group' export { ChatContent } from './chat-content' export { Options } from './options' export { PendingTagIndicator, parseSpecialTags, SpecialTags } from './special-tags' -export { ThinkingBlock } from './thinking-block' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts deleted file mode 100644 index 4b82db6a47d..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ThinkingBlock } from './thinking-block' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx deleted file mode 100644 index 208a358b975..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx +++ /dev/null @@ -1,108 +0,0 @@ -'use client' - -import { useEffect, useLayoutEffect, useRef, useState } from 'react' -import { Blimp, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn' -import { cn } from '@/lib/core/utils/cn' - -interface ThinkingBlockProps { - content: string - isActive: boolean - isStreaming?: boolean - startedAt?: number -} - -const MIN_VISIBLE_THINKING_MS = 3000 - -export function ThinkingBlock({ - content, - isActive, - isStreaming = false, - startedAt, -}: ThinkingBlockProps) { - // Start collapsed so the `Expandable` plays its height-open animation - // when `expanded` flips to true below — otherwise the panel mounts - // already-open and jumps up with its full content in one frame. - const [expanded, setExpanded] = useState(false) - const panelRef = useRef(null) - const wasActiveRef = useRef(null) - // Suppress active thinking until it exceeds MIN_VISIBLE_THINKING_MS. - // Completed-<=threshold is filtered upstream in message-content, so if - // we're mounted with isActive=false we've already passed that gate. - const [thresholdReached, setThresholdReached] = useState(() => { - if (!isActive || startedAt === undefined) return true - return Date.now() - startedAt > MIN_VISIBLE_THINKING_MS - }) - - useEffect(() => { - if (thresholdReached) return - if (!isActive || startedAt === undefined) { - setThresholdReached(true) - return - } - const remainingMs = Math.max(0, MIN_VISIBLE_THINKING_MS - (Date.now() - startedAt)) - const id = window.setTimeout(() => setThresholdReached(true), remainingMs + 50) - return () => window.clearTimeout(id) - }, [isActive, startedAt, thresholdReached]) - - useEffect(() => { - // Wait until the threshold has actually been reached — otherwise this - // effect fires during the 3-second hidden period (while the component - // returns null) and sets `expanded` to true before the panel is even - // rendered, so the Collapsible mounts already-open with no animation. - if (!thresholdReached) return - if (wasActiveRef.current === isActive) return - // On first run (wasActiveRef === null): open if the stream is live — - // even when thinking itself has already ended — so a mid-stream refresh - // shows the thinking panel open while the rest of the response is still - // being generated. Subsequent runs only react to the isActive transition - // (auto-collapse when thinking ends). - const isFirstRun = wasActiveRef.current === null - wasActiveRef.current = isActive - const target = isFirstRun ? isActive || isStreaming : isActive - // Defer to the next frame so Radix Collapsible paints the closed state - // first, then sees the transition to open. Without this, React can batch - // the mount + flip into a single commit and the animation never plays. - const id = window.requestAnimationFrame(() => setExpanded(target)) - return () => window.cancelAnimationFrame(id) - }, [isActive, isStreaming, thresholdReached]) - - useLayoutEffect(() => { - if (!isActive || !expanded) return - const el = panelRef.current - if (!el) return - el.scrollTop = el.scrollHeight - }, [content, isActive, expanded]) - - if (!thresholdReached) return null - - return ( -
- - - - -
-
- {content} -
-
-
-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index a8a0ae35b9d..e4286bf5e22 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -10,14 +10,7 @@ import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/ch import type { ContentBlock, OptionItem, ToolCallData } from '../../types' import { SUBAGENT_LABELS } from '../../types' import type { AgentGroupItem } from './components' -import { - AgentGroup, - ChatContent, - CircleStop, - Options, - PendingTagIndicator, - ThinkingBlock, -} from './components' +import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components' import { deriveMessagePhase, isToolDone, type MessagePhase } from './utils' const FILE_SUBAGENT_ID = 'file' @@ -29,14 +22,6 @@ interface TextSegment { content: string } -interface ThinkingSegment { - type: 'thinking' - id: string - content: string - startedAt?: number - endedAt?: number -} - interface AgentGroupSegment { type: 'agent_group' id: string @@ -56,12 +41,7 @@ interface StoppedSegment { type: 'stopped' } -type MessageSegment = - | TextSegment - | ThinkingSegment - | AgentGroupSegment - | OptionsSegment - | StoppedSegment +type MessageSegment = TextSegment | AgentGroupSegment | OptionsSegment | StoppedSegment const SUBAGENT_KEYS = new Set(Object.keys(SUBAGENT_LABELS)) @@ -279,23 +259,9 @@ function parseBlocksWithSpanTree(blocks: ContentBlock[]): MessageSegment[] { continue } - if (block.type === 'thinking') { - if (!block.content?.trim()) continue - const last = segments[segments.length - 1] - if (last?.type === 'thinking' && last.endedAt === undefined) { - last.content += block.content - if (block.endedAt !== undefined) last.endedAt = block.endedAt - } else { - segments.push({ - type: 'thinking', - id: `thinking-${i}`, - content: block.content, - startedAt: block.timestamp, - endedAt: block.endedAt, - }) - } - continue - } + // Main-agent thinking is intentionally not rendered. The reasoning is still + // reduced and persisted upstream — this is a display-only omission. + if (block.type === 'thinking') continue if (block.type === 'text') { if (!block.content) continue @@ -515,21 +481,10 @@ function parseBlocksLegacy(blocks: ContentBlock[]): MessageSegment[] { } if (block.type === 'thinking') { + // Main-agent thinking is not rendered, but it still breaks open subagent + // lanes so later chunks don't merge across it (display-only omission). if (!block.content?.trim()) continue flushLanes() - const last = segments[segments.length - 1] - if (last?.type === 'thinking' && last.endedAt === undefined) { - last.content += block.content - if (block.endedAt !== undefined) last.endedAt = block.endedAt - } else { - segments.push({ - type: 'thinking', - id: `thinking-${i}`, - content: block.content, - startedAt: block.timestamp, - endedAt: block.endedAt, - }) - } continue } @@ -776,29 +731,6 @@ function MessageContentInner({ } /> ) - case 'thinking': { - const isActive = - isStreaming && i === segments.length - 1 && segment.endedAt === undefined - const elapsedMs = - segment.startedAt !== undefined && segment.endedAt !== undefined - ? segment.endedAt - segment.startedAt - : undefined - // Hide completed thinking that took 3s or less — quick thinking - // isn't worth the visual noise. Still show while active (unknown - // duration yet) and still show when timing is missing (old - // persisted blocks) so we don't drop historical content. - if (elapsedMs !== undefined && elapsedMs <= 3000) return null - return ( -
- -
- ) - } case 'agent_group': { return (
@@ -809,7 +741,8 @@ function MessageContentInner({ items={segment.items} isDelegating={segment.isDelegating} isStreaming={isStreaming} - defaultExpanded={segment.isOpen} + isCurrentSection={i === segments.length - 1} + isLaneOpen={segment.isOpen} />
) diff --git a/apps/sim/blocks/blocks/sportmonks.ts b/apps/sim/blocks/blocks/sportmonks.ts index 4939bd51778..4fc5f097db8 100644 --- a/apps/sim/blocks/blocks/sportmonks.ts +++ b/apps/sim/blocks/blocks/sportmonks.ts @@ -13,104 +13,801 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n generationType: 'timestamp' as const, } -const FOOTBALL_OPS = [ - 'football_get_livescores', - 'football_get_inplay_livescores', +const INCLUDE_OPS = [ + 'football_get_team', + 'football_get_totw', + 'core_get_cities', + 'core_get_city', + 'core_get_continent', + 'core_get_continents', + 'core_get_countries', + 'core_get_country', + 'core_get_region', + 'core_get_regions', + 'core_search_cities', + 'core_search_countries', + 'core_search_regions', + 'football_expected_by_player', + 'football_expected_by_team', + 'football_get_all_commentaries', + 'football_get_all_fixtures', + 'football_get_all_players', + 'football_get_all_rivals', + 'football_get_all_teams', + 'football_get_all_transfer_rumours', + 'football_get_all_transfers', + 'football_get_brackets_by_season', + 'football_get_coach', + 'football_get_coaches', + 'football_get_coaches_by_country', + 'football_get_commentaries_by_fixture', + 'football_get_current_leagues_by_team', + 'football_get_expected_lineups_by_player', + 'football_get_expected_lineups_by_team', + 'football_get_extended_team_squad', + 'football_get_fixture', 'football_get_fixtures_by_date', 'football_get_fixtures_by_date_range', - 'football_get_fixture', + 'football_get_fixtures_by_date_range_for_team', + 'football_get_fixtures_by_ids', + 'football_get_grouped_standings_by_round', 'football_get_head_to_head', - 'football_get_leagues', + 'football_get_inplay_livescores', + 'football_get_latest_coaches', + 'football_get_latest_fixtures', + 'football_get_latest_livescores', + 'football_get_latest_players', + 'football_get_latest_totw', + 'football_get_latest_transfers', 'football_get_league', - 'football_search_teams', - 'football_get_team', - 'football_get_team_squad', - 'football_search_players', + 'football_get_leagues', + 'football_get_leagues_by_country', + 'football_get_leagues_by_date', + 'football_get_leagues_by_team', + 'football_get_live_leagues', + 'football_get_live_probabilities', + 'football_get_live_probabilities_by_fixture', + 'football_get_live_standings_by_league', + 'football_get_livescores', + 'football_get_match_facts', + 'football_get_match_facts_by_date_range', + 'football_get_match_facts_by_fixture', + 'football_get_match_facts_by_league', + 'football_get_past_fixtures_by_tv_station', 'football_get_player', + 'football_get_players_by_country', + 'football_get_postmatch_news', + 'football_get_postmatch_news_by_season', + 'football_get_predictability_by_league', + 'football_get_prematch_news', + 'football_get_prematch_news_by_season', + 'football_get_prematch_news_upcoming', + 'football_get_probabilities', + 'football_get_probabilities_by_fixture', + 'football_get_referee', + 'football_get_referees', + 'football_get_referees_by_country', + 'football_get_referees_by_season', + 'football_get_rivals_by_team', + 'football_get_round', + 'football_get_round_statistics', + 'football_get_rounds', + 'football_get_rounds_by_season', + 'football_get_season', + 'football_get_seasons', + 'football_get_seasons_by_team', + 'football_get_stage', + 'football_get_stage_statistics', + 'football_get_stages', + 'football_get_stages_by_season', + 'football_get_standing_corrections_by_season', + 'football_get_standings', + 'football_get_standings_by_round', 'football_get_standings_by_season', + 'football_get_team_rankings', + 'football_get_team_rankings_by_date', + 'football_get_team_rankings_by_team', + 'football_get_team_squad', + 'football_get_team_squad_by_season', + 'football_get_teams_by_country', + 'football_get_teams_by_season', 'football_get_topscorers_by_season', -] - -const MOTORSPORT_OPS = [ - 'motorsport_get_livescores', - 'motorsport_get_fixtures_by_date', - 'motorsport_get_fixture', - 'motorsport_get_drivers', + 'football_get_topscorers_by_stage', + 'football_get_totw_by_round', + 'football_get_transfer', + 'football_get_transfer_rumour', + 'football_get_transfer_rumours_between_dates', + 'football_get_transfer_rumours_by_player', + 'football_get_transfer_rumours_by_team', + 'football_get_transfers_between_dates', + 'football_get_transfers_by_player', + 'football_get_transfers_by_team', + 'football_get_tv_station', + 'football_get_tv_stations', + 'football_get_tv_stations_by_fixture', + 'football_get_upcoming_fixtures_by_market', + 'football_get_upcoming_fixtures_by_tv_station', + 'football_get_value_bets', + 'football_get_value_bets_by_fixture', + 'football_get_venue', + 'football_get_venues', + 'football_get_venues_by_season', + 'football_search_coaches', + 'football_search_fixtures', + 'football_search_leagues', + 'football_search_players', + 'football_search_referees', + 'football_search_rounds', + 'football_search_seasons', + 'football_search_stages', + 'football_search_teams', + 'football_search_venues', + 'motorsport_get_all_fixtures', + 'motorsport_get_current_leagues_by_team', 'motorsport_get_driver', - 'motorsport_search_drivers', - 'motorsport_get_teams', - 'motorsport_get_team', + 'motorsport_get_driver_standings', 'motorsport_get_driver_standings_by_season', - 'motorsport_get_team_standings_by_season', + 'motorsport_get_drivers', + 'motorsport_get_drivers_by_country', + 'motorsport_get_drivers_by_season', + 'motorsport_get_fixture', + 'motorsport_get_fixtures_by_date', + 'motorsport_get_fixtures_by_date_range', + 'motorsport_get_fixtures_by_ids', 'motorsport_get_laps_by_fixture', + 'motorsport_get_laps_by_fixture_and_driver', + 'motorsport_get_laps_by_fixture_and_lap', + 'motorsport_get_latest_laps_by_fixture', + 'motorsport_get_latest_pitstops_by_fixture', + 'motorsport_get_latest_stints_by_fixture', + 'motorsport_get_latest_updated_drivers', + 'motorsport_get_latest_updated_fixtures', + 'motorsport_get_league', + 'motorsport_get_leagues', + 'motorsport_get_leagues_by_country', + 'motorsport_get_leagues_by_date', + 'motorsport_get_leagues_by_live', + 'motorsport_get_leagues_by_team', + 'motorsport_get_livescores', 'motorsport_get_pitstops_by_fixture', + 'motorsport_get_pitstops_by_fixture_and_driver', + 'motorsport_get_pitstops_by_fixture_and_lap', + 'motorsport_get_race_results_by_season_and_driver', + 'motorsport_get_race_results_by_season_and_team', + 'motorsport_get_schedules_by_season', + 'motorsport_get_season', + 'motorsport_get_seasons', + 'motorsport_get_stage', + 'motorsport_get_stages', + 'motorsport_get_stages_by_season', + 'motorsport_get_state', + 'motorsport_get_states', + 'motorsport_get_stints_by_fixture', + 'motorsport_get_stints_by_fixture_and_driver', + 'motorsport_get_stints_by_fixture_and_stint', + 'motorsport_get_team', + 'motorsport_get_team_standings', + 'motorsport_get_team_standings_by_season', + 'motorsport_get_teams', + 'motorsport_get_teams_by_country', + 'motorsport_get_teams_by_season', + 'motorsport_get_venue', + 'motorsport_get_venues', + 'motorsport_get_venues_by_season', + 'motorsport_search_drivers', + 'motorsport_search_leagues', + 'motorsport_search_stages', + 'motorsport_search_teams', + 'motorsport_search_venues', + 'odds_get_all_historical_odds', + 'odds_get_all_inplay_odds', + 'odds_get_all_pre_match_odds', + 'odds_get_all_premium_odds', + 'odds_get_inplay_odds_by_fixture', + 'odds_get_inplay_odds_by_fixture_and_bookmaker', + 'odds_get_inplay_odds_by_fixture_and_market', + 'odds_get_last_updated_inplay_odds', + 'odds_get_last_updated_pre_match_odds', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_pre_match_odds_by_fixture_and_bookmaker', + 'odds_get_pre_match_odds_by_fixture_and_market', + 'odds_get_premium_odds_by_fixture', + 'odds_get_premium_odds_by_fixture_and_bookmaker', + 'odds_get_premium_odds_by_fixture_and_market', + 'odds_get_updated_historical_odds_between', + 'odds_get_updated_premium_odds_between', ] -const CORE_GEO_OPS = [ - 'core_get_continents', - 'core_get_continent', +const FILTER_OPS = [ + 'football_get_team', + 'core_get_cities', 'core_get_countries', - 'core_get_country', - 'core_search_countries', 'core_get_regions', - 'core_get_region', - 'core_get_cities', - 'core_get_city', 'core_search_cities', -] - -const ODDS_FIXTURE_OPS = ['odds_get_pre_match_odds_by_fixture', 'odds_get_inplay_odds_by_fixture'] - -const INCLUDE_OPS = [...FOOTBALL_OPS, ...MOTORSPORT_OPS, ...ODDS_FIXTURE_OPS, ...CORE_GEO_OPS] - -const FILTER_OPS = [ - ...FOOTBALL_OPS, - ...MOTORSPORT_OPS, - ...ODDS_FIXTURE_OPS, + 'core_search_countries', + 'core_search_regions', + 'football_expected_by_player', + 'football_expected_by_team', + 'football_get_all_commentaries', + 'football_get_all_fixtures', + 'football_get_all_players', + 'football_get_all_rivals', + 'football_get_all_teams', + 'football_get_all_transfer_rumours', + 'football_get_all_transfers', + 'football_get_coach', + 'football_get_coaches', + 'football_get_coaches_by_country', + 'football_get_current_leagues_by_team', + 'football_get_expected_lineups_by_player', + 'football_get_expected_lineups_by_team', + 'football_get_extended_team_squad', + 'football_get_fixture', + 'football_get_fixtures_by_date', + 'football_get_fixtures_by_date_range', + 'football_get_fixtures_by_date_range_for_team', + 'football_get_fixtures_by_ids', + 'football_get_grouped_standings_by_round', + 'football_get_head_to_head', + 'football_get_inplay_livescores', + 'football_get_latest_coaches', + 'football_get_latest_fixtures', + 'football_get_latest_livescores', + 'football_get_latest_players', + 'football_get_latest_transfers', + 'football_get_league', + 'football_get_leagues', + 'football_get_leagues_by_country', + 'football_get_leagues_by_date', + 'football_get_leagues_by_team', + 'football_get_live_leagues', + 'football_get_live_standings_by_league', + 'football_get_livescores', + 'football_get_match_facts', + 'football_get_match_facts_by_date_range', + 'football_get_match_facts_by_fixture', + 'football_get_match_facts_by_league', + 'football_get_past_fixtures_by_tv_station', + 'football_get_player', + 'football_get_players_by_country', + 'football_get_postmatch_news', + 'football_get_postmatch_news_by_season', + 'football_get_predictability_by_league', + 'football_get_prematch_news', + 'football_get_prematch_news_by_season', + 'football_get_prematch_news_upcoming', + 'football_get_probabilities', + 'football_get_probabilities_by_fixture', + 'football_get_referee', + 'football_get_referees', + 'football_get_referees_by_country', + 'football_get_referees_by_season', + 'football_get_round', + 'football_get_round_statistics', + 'football_get_rounds', + 'football_get_rounds_by_season', + 'football_get_season', + 'football_get_seasons', + 'football_get_seasons_by_team', + 'football_get_stage', + 'football_get_stage_statistics', + 'football_get_stages', + 'football_get_stages_by_season', + 'football_get_standing_corrections_by_season', + 'football_get_standings', + 'football_get_standings_by_round', + 'football_get_standings_by_season', + 'football_get_team_squad', + 'football_get_team_squad_by_season', + 'football_get_teams_by_country', + 'football_get_teams_by_season', + 'football_get_topscorers_by_season', + 'football_get_topscorers_by_stage', + 'football_get_transfer', + 'football_get_transfer_rumour', + 'football_get_transfer_rumours_between_dates', + 'football_get_transfer_rumours_by_player', + 'football_get_transfer_rumours_by_team', + 'football_get_transfers_between_dates', + 'football_get_transfers_by_player', + 'football_get_transfers_by_team', + 'football_get_tv_station', + 'football_get_tv_stations', + 'football_get_tv_stations_by_fixture', + 'football_get_upcoming_fixtures_by_market', + 'football_get_upcoming_fixtures_by_tv_station', + 'football_get_venue', + 'football_get_venues', + 'football_get_venues_by_season', + 'football_search_coaches', + 'football_search_fixtures', + 'football_search_leagues', + 'football_search_players', + 'football_search_referees', + 'football_search_rounds', + 'football_search_seasons', + 'football_search_stages', + 'football_search_teams', + 'football_search_venues', + 'motorsport_get_all_fixtures', + 'motorsport_get_current_leagues_by_team', + 'motorsport_get_driver', + 'motorsport_get_driver_standings', + 'motorsport_get_driver_standings_by_season', + 'motorsport_get_drivers', + 'motorsport_get_drivers_by_country', + 'motorsport_get_drivers_by_season', + 'motorsport_get_fixture', + 'motorsport_get_fixtures_by_date', + 'motorsport_get_fixtures_by_date_range', + 'motorsport_get_fixtures_by_ids', + 'motorsport_get_laps_by_fixture', + 'motorsport_get_laps_by_fixture_and_driver', + 'motorsport_get_laps_by_fixture_and_lap', + 'motorsport_get_latest_laps_by_fixture', + 'motorsport_get_latest_pitstops_by_fixture', + 'motorsport_get_latest_stints_by_fixture', + 'motorsport_get_latest_updated_drivers', + 'motorsport_get_latest_updated_fixtures', + 'motorsport_get_league', + 'motorsport_get_leagues', + 'motorsport_get_leagues_by_country', + 'motorsport_get_leagues_by_date', + 'motorsport_get_leagues_by_live', + 'motorsport_get_leagues_by_team', + 'motorsport_get_livescores', + 'motorsport_get_pitstops_by_fixture', + 'motorsport_get_pitstops_by_fixture_and_driver', + 'motorsport_get_pitstops_by_fixture_and_lap', + 'motorsport_get_race_results_by_season_and_driver', + 'motorsport_get_race_results_by_season_and_team', + 'motorsport_get_schedules_by_season', + 'motorsport_get_season', + 'motorsport_get_seasons', + 'motorsport_get_stage', + 'motorsport_get_stages', + 'motorsport_get_stages_by_season', + 'motorsport_get_state', + 'motorsport_get_states', + 'motorsport_get_stints_by_fixture', + 'motorsport_get_stints_by_fixture_and_driver', + 'motorsport_get_stints_by_fixture_and_stint', + 'motorsport_get_team', + 'motorsport_get_team_standings', + 'motorsport_get_team_standings_by_season', + 'motorsport_get_teams', + 'motorsport_get_teams_by_country', + 'motorsport_get_teams_by_season', + 'motorsport_get_venue', + 'motorsport_get_venues', + 'motorsport_get_venues_by_season', + 'motorsport_search_drivers', + 'motorsport_search_leagues', + 'motorsport_search_stages', + 'motorsport_search_teams', + 'motorsport_search_venues', + 'odds_get_all_historical_odds', + 'odds_get_all_inplay_odds', + 'odds_get_all_pre_match_odds', + 'odds_get_all_premium_odds', 'odds_get_bookmakers', + 'odds_get_inplay_odds_by_fixture', + 'odds_get_inplay_odds_by_fixture_and_bookmaker', + 'odds_get_inplay_odds_by_fixture_and_market', + 'odds_get_last_updated_inplay_odds', + 'odds_get_last_updated_pre_match_odds', 'odds_get_markets', - 'core_get_continents', - 'core_get_countries', - 'core_get_regions', - 'core_get_cities', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_pre_match_odds_by_fixture_and_bookmaker', + 'odds_get_pre_match_odds_by_fixture_and_market', + 'odds_get_premium_odds_by_fixture', + 'odds_get_premium_odds_by_fixture_and_bookmaker', + 'odds_get_premium_odds_by_fixture_and_market', + 'odds_get_updated_historical_odds_between', + 'odds_get_updated_premium_odds_between', ] const PAGINATED_OPS = [ + 'football_get_totw', + 'core_get_cities', + 'core_get_continents', + 'core_get_countries', + 'core_get_my_usage', + 'core_get_regions', + 'core_get_types', + 'core_search_cities', + 'core_search_countries', + 'core_search_regions', + 'football_expected_by_player', + 'football_expected_by_team', + 'football_get_all_commentaries', + 'football_get_all_fixtures', + 'football_get_all_players', + 'football_get_all_rivals', + 'football_get_all_teams', + 'football_get_all_transfer_rumours', + 'football_get_all_transfers', + 'football_get_coaches', + 'football_get_coaches_by_country', + 'football_get_current_leagues_by_team', + 'football_get_expected_lineups_by_player', + 'football_get_expected_lineups_by_team', 'football_get_fixtures_by_date', 'football_get_fixtures_by_date_range', + 'football_get_fixtures_by_date_range_for_team', 'football_get_head_to_head', + 'football_get_latest_coaches', + 'football_get_latest_transfers', 'football_get_leagues', - 'football_search_teams', - 'football_search_players', + 'football_get_leagues_by_country', + 'football_get_leagues_by_date', + 'football_get_leagues_by_team', + 'football_get_live_leagues', + 'football_get_live_probabilities', + 'football_get_live_probabilities_by_fixture', + 'football_get_match_facts', + 'football_get_match_facts_by_date_range', + 'football_get_match_facts_by_fixture', + 'football_get_match_facts_by_league', + 'football_get_past_fixtures_by_tv_station', + 'football_get_players_by_country', + 'football_get_postmatch_news', + 'football_get_postmatch_news_by_season', + 'football_get_predictability_by_league', + 'football_get_prematch_news', + 'football_get_prematch_news_by_season', + 'football_get_prematch_news_upcoming', + 'football_get_probabilities', + 'football_get_probabilities_by_fixture', + 'football_get_referees', + 'football_get_referees_by_country', + 'football_get_referees_by_season', + 'football_get_round_statistics', + 'football_get_rounds', + 'football_get_seasons', + 'football_get_stage_statistics', + 'football_get_stages', + 'football_get_standings', + 'football_get_states', + 'football_get_team_rankings', + 'football_get_team_rankings_by_date', + 'football_get_team_rankings_by_team', + 'football_get_teams_by_country', + 'football_get_teams_by_season', 'football_get_topscorers_by_season', - 'motorsport_get_livescores', - 'motorsport_get_fixtures_by_date', - 'motorsport_get_drivers', - 'motorsport_search_drivers', - 'motorsport_get_teams', + 'football_get_topscorers_by_stage', + 'football_get_transfer_rumours_between_dates', + 'football_get_transfer_rumours_by_player', + 'football_get_transfer_rumours_by_team', + 'football_get_transfers_between_dates', + 'football_get_transfers_by_player', + 'football_get_transfers_by_team', + 'football_get_tv_stations', + 'football_get_tv_stations_by_fixture', + 'football_get_upcoming_fixtures_by_market', + 'football_get_upcoming_fixtures_by_tv_station', + 'football_get_value_bets', + 'football_get_value_bets_by_fixture', + 'football_get_venues', + 'football_search_coaches', + 'football_search_fixtures', + 'football_search_leagues', + 'football_search_players', + 'football_search_referees', + 'football_search_rounds', + 'football_search_seasons', + 'football_search_stages', + 'football_search_teams', + 'football_search_venues', + 'motorsport_get_all_fixtures', + 'motorsport_get_current_leagues_by_team', + 'motorsport_get_driver_standings', 'motorsport_get_driver_standings_by_season', + 'motorsport_get_drivers', + 'motorsport_get_drivers_by_country', + 'motorsport_get_drivers_by_season', + 'motorsport_get_fixtures_by_date', + 'motorsport_get_fixtures_by_date_range', + 'motorsport_get_fixtures_by_ids', + 'motorsport_get_latest_updated_drivers', + 'motorsport_get_latest_updated_fixtures', + 'motorsport_get_leagues', + 'motorsport_get_leagues_by_country', + 'motorsport_get_leagues_by_date', + 'motorsport_get_leagues_by_live', + 'motorsport_get_leagues_by_team', + 'motorsport_get_livescores', + 'motorsport_get_race_results_by_season_and_driver', + 'motorsport_get_race_results_by_season_and_team', + 'motorsport_get_schedules_by_season', + 'motorsport_get_seasons', + 'motorsport_get_stages', + 'motorsport_get_stages_by_season', + 'motorsport_get_states', + 'motorsport_get_team_standings', 'motorsport_get_team_standings_by_season', - 'odds_get_pre_match_odds_by_fixture', - 'odds_get_inplay_odds_by_fixture', + 'motorsport_get_teams', + 'motorsport_get_teams_by_country', + 'motorsport_get_teams_by_season', + 'motorsport_get_venues', + 'motorsport_get_venues_by_season', + 'motorsport_search_drivers', + 'motorsport_search_leagues', + 'motorsport_search_stages', + 'motorsport_search_teams', + 'motorsport_search_venues', + 'odds_get_all_historical_odds', + 'odds_get_all_inplay_odds', + 'odds_get_all_pre_match_odds', + 'odds_get_all_premium_odds', + 'odds_get_bookmaker_event_ids_by_fixture', 'odds_get_bookmakers', - 'odds_search_bookmakers', + 'odds_get_bookmakers_by_fixture', + 'odds_get_inplay_odds_by_fixture', 'odds_get_markets', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_updated_historical_odds_between', + 'odds_get_updated_premium_odds_between', + 'odds_search_bookmakers', 'odds_search_markets', - 'core_get_continents', - 'core_get_countries', - 'core_search_countries', - 'core_get_regions', - 'core_get_cities', +] + +const BOOKMAKER_ID_OPS = [ + 'odds_get_bookmaker', + 'odds_get_inplay_odds_by_fixture_and_bookmaker', + 'odds_get_pre_match_odds_by_fixture_and_bookmaker', + 'odds_get_premium_odds_by_fixture_and_bookmaker', +] + +const CITY_ID_OPS = ['core_get_city'] + +const COACH_ID_OPS = ['football_get_coach'] + +const CONTINENT_ID_OPS = ['core_get_continent'] + +const COUNTRY_ID_OPS = [ + 'core_get_country', + 'football_get_coaches_by_country', + 'football_get_leagues_by_country', + 'football_get_players_by_country', + 'football_get_referees_by_country', + 'football_get_teams_by_country', + 'motorsport_get_drivers_by_country', + 'motorsport_get_leagues_by_country', + 'motorsport_get_teams_by_country', +] + +const DATE_OPS = [ + 'football_get_fixtures_by_date', + 'football_get_leagues_by_date', + 'football_get_team_rankings_by_date', + 'motorsport_get_fixtures_by_date', + 'motorsport_get_leagues_by_date', +] + +const DRIVER_ID_OPS = [ + 'motorsport_get_driver', + 'motorsport_get_laps_by_fixture_and_driver', + 'motorsport_get_pitstops_by_fixture_and_driver', + 'motorsport_get_race_results_by_season_and_driver', + 'motorsport_get_stints_by_fixture_and_driver', +] + +const END_DATE_OPS = [ + 'football_get_fixtures_by_date_range', + 'football_get_fixtures_by_date_range_for_team', + 'football_get_match_facts_by_date_range', + 'football_get_transfer_rumours_between_dates', + 'football_get_transfers_between_dates', + 'motorsport_get_fixtures_by_date_range', +] + +const FIXTURE_ID_OPS = [ + 'football_get_commentaries_by_fixture', + 'football_get_fixture', + 'football_get_live_probabilities_by_fixture', + 'football_get_match_facts_by_fixture', + 'football_get_probabilities_by_fixture', + 'football_get_tv_stations_by_fixture', + 'football_get_value_bets_by_fixture', + 'motorsport_get_fixture', + 'motorsport_get_laps_by_fixture', + 'motorsport_get_laps_by_fixture_and_driver', + 'motorsport_get_laps_by_fixture_and_lap', + 'motorsport_get_latest_laps_by_fixture', + 'motorsport_get_latest_pitstops_by_fixture', + 'motorsport_get_latest_stints_by_fixture', + 'motorsport_get_pitstops_by_fixture', + 'motorsport_get_pitstops_by_fixture_and_driver', + 'motorsport_get_pitstops_by_fixture_and_lap', + 'motorsport_get_stints_by_fixture', + 'motorsport_get_stints_by_fixture_and_driver', + 'motorsport_get_stints_by_fixture_and_stint', + 'odds_get_bookmaker_event_ids_by_fixture', + 'odds_get_bookmakers_by_fixture', + 'odds_get_inplay_odds_by_fixture', + 'odds_get_inplay_odds_by_fixture_and_bookmaker', + 'odds_get_inplay_odds_by_fixture_and_market', + 'odds_get_pre_match_odds_by_fixture', + 'odds_get_pre_match_odds_by_fixture_and_bookmaker', + 'odds_get_pre_match_odds_by_fixture_and_market', + 'odds_get_premium_odds_by_fixture', + 'odds_get_premium_odds_by_fixture_and_bookmaker', + 'odds_get_premium_odds_by_fixture_and_market', +] + +const FIXTURE_IDS_OPS = ['motorsport_get_fixtures_by_ids'] + +const FROM_TIMESTAMP_OPS = [ + 'odds_get_updated_historical_odds_between', + 'odds_get_updated_premium_odds_between', +] + +const IDS_OPS = ['football_get_fixtures_by_ids'] + +const LAP_NUMBER_OPS = [ + 'motorsport_get_laps_by_fixture_and_lap', + 'motorsport_get_pitstops_by_fixture_and_lap', +] + +const LEAGUE_ID_OPS = [ + 'football_get_latest_totw', + 'football_get_league', + 'football_get_live_standings_by_league', + 'football_get_match_facts_by_league', + 'football_get_predictability_by_league', + 'motorsport_get_league', +] + +const MARKET_ID_OPS = [ + 'football_get_upcoming_fixtures_by_market', + 'odds_get_inplay_odds_by_fixture_and_market', + 'odds_get_market', + 'odds_get_pre_match_odds_by_fixture_and_market', + 'odds_get_premium_odds_by_fixture_and_market', +] + +const PLAYER_ID_OPS = [ + 'football_get_expected_lineups_by_player', + 'football_get_player', + 'football_get_transfer_rumours_by_player', + 'football_get_transfers_by_player', +] + +const QUERY_OPS = [ 'core_search_cities', - 'core_get_types', + 'core_search_countries', + 'core_search_regions', + 'football_search_coaches', + 'football_search_fixtures', + 'football_search_leagues', + 'football_search_players', + 'football_search_referees', + 'football_search_rounds', + 'football_search_seasons', + 'football_search_stages', + 'football_search_teams', + 'football_search_venues', + 'motorsport_search_drivers', + 'motorsport_search_leagues', + 'motorsport_search_stages', + 'motorsport_search_teams', + 'motorsport_search_venues', + 'odds_search_bookmakers', + 'odds_search_markets', +] + +const REFEREE_ID_OPS = ['football_get_referee'] + +const REGION_ID_OPS = ['core_get_region'] + +const ROUND_ID_OPS = [ + 'football_get_grouped_standings_by_round', + 'football_get_round', + 'football_get_round_statistics', + 'football_get_standings_by_round', + 'football_get_totw_by_round', +] + +const RUMOUR_ID_OPS = ['football_get_transfer_rumour'] + +const SEASON_ID_OPS = [ + 'football_get_brackets_by_season', + 'football_get_postmatch_news_by_season', + 'football_get_prematch_news_by_season', + 'football_get_referees_by_season', + 'football_get_rounds_by_season', + 'football_get_schedules_by_season', + 'football_get_schedules_by_season_and_team', + 'football_get_season', + 'football_get_stages_by_season', + 'football_get_standing_corrections_by_season', + 'football_get_standings_by_season', + 'football_get_team_squad_by_season', + 'football_get_teams_by_season', + 'football_get_topscorers_by_season', + 'football_get_venues_by_season', + 'motorsport_get_driver_standings_by_season', + 'motorsport_get_drivers_by_season', + 'motorsport_get_race_results_by_season_and_driver', + 'motorsport_get_race_results_by_season_and_team', + 'motorsport_get_schedules_by_season', + 'motorsport_get_season', + 'motorsport_get_stages_by_season', + 'motorsport_get_team_standings_by_season', + 'motorsport_get_teams_by_season', + 'motorsport_get_venues_by_season', +] + +const STAGE_ID_OPS = [ + 'football_get_stage', + 'football_get_stage_statistics', + 'football_get_topscorers_by_stage', + 'motorsport_get_stage', +] + +const START_DATE_OPS = [ + 'football_get_fixtures_by_date_range', + 'football_get_fixtures_by_date_range_for_team', + 'football_get_match_facts_by_date_range', + 'football_get_transfer_rumours_between_dates', + 'football_get_transfers_between_dates', + 'motorsport_get_fixtures_by_date_range', +] + +const STATE_ID_OPS = ['football_get_state', 'motorsport_get_state'] + +const STINT_NUMBER_OPS = ['motorsport_get_stints_by_fixture_and_stint'] + +const TEAM_1_OPS = ['football_get_head_to_head'] + +const TEAM_2_OPS = ['football_get_head_to_head'] + +const TEAM_ID_OPS = [ + 'football_get_team', + 'football_get_current_leagues_by_team', + 'football_get_expected_lineups_by_team', + 'football_get_extended_team_squad', + 'football_get_fixtures_by_date_range_for_team', + 'football_get_leagues_by_team', + 'football_get_rivals_by_team', + 'football_get_schedules_by_season_and_team', + 'football_get_schedules_by_team', + 'football_get_seasons_by_team', + 'football_get_team_rankings_by_team', + 'football_get_team_squad', + 'football_get_team_squad_by_season', + 'football_get_transfer_rumours_by_team', + 'football_get_transfers_by_team', + 'motorsport_get_current_leagues_by_team', + 'motorsport_get_leagues_by_team', + 'motorsport_get_race_results_by_season_and_team', + 'motorsport_get_team', ] +const TO_TIMESTAMP_OPS = [ + 'odds_get_updated_historical_odds_between', + 'odds_get_updated_premium_odds_between', +] + +const TRANSFER_ID_OPS = ['football_get_transfer'] + +const TV_STATION_ID_OPS = [ + 'football_get_past_fixtures_by_tv_station', + 'football_get_tv_station', + 'football_get_upcoming_fixtures_by_tv_station', +] + +const TYPE_ID_OPS = ['core_get_type'] + +const VENUE_ID_OPS = ['football_get_venue', 'motorsport_get_venue'] + export const SportmonksBlock: BlockConfig = { type: 'sportmonks', name: 'Sportmonks', description: 'Access Sportmonks football, motorsport, odds, and reference data', longDescription: - 'Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, teams, squads, players, standings, and topscorers. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones.', + 'Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, seasons, stages, rounds, teams, squads, players, coaches, referees, venues, standings, topscorers, transfers, schedules, commentaries, TV stations, rivals, expected goals (xG), and predictions. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones.', docsLink: 'https://docs.sim.ai/integrations/sportmonks', category: 'tools', integrationType: IntegrationType.Analytics, @@ -123,358 +820,1057 @@ export const SportmonksBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ - // Football - { label: 'Get Live Football Scores', id: 'football_get_livescores', group: 'Football' }, { - label: 'Get Inplay Football Scores', - id: 'football_get_inplay_livescores', + label: 'Football: Get Expected xG by Player', + id: 'football_expected_by_player', + group: 'Football', + }, + { + label: 'Football: Get Expected xG by Team', + id: 'football_expected_by_team', + group: 'Football', + }, + { + label: 'Football: Get All Commentaries', + id: 'football_get_all_commentaries', + group: 'Football', + }, + { label: 'Football: Get All Fixtures', id: 'football_get_all_fixtures', group: 'Football' }, + { label: 'Football: Get All Players', id: 'football_get_all_players', group: 'Football' }, + { label: 'Football: Get All Rivals', id: 'football_get_all_rivals', group: 'Football' }, + { label: 'Football: Get All Teams', id: 'football_get_all_teams', group: 'Football' }, + { + label: 'Football: Get All Transfer Rumours', + id: 'football_get_all_transfer_rumours', + group: 'Football', + }, + { + label: 'Football: Get All Transfers', + id: 'football_get_all_transfers', + group: 'Football', + }, + { + label: 'Football: Get Brackets by Season', + id: 'football_get_brackets_by_season', + group: 'Football', + }, + { label: 'Football: Get Coach by ID', id: 'football_get_coach', group: 'Football' }, + { label: 'Football: Get Coaches', id: 'football_get_coaches', group: 'Football' }, + { + label: 'Football: Get Coaches by Country', + id: 'football_get_coaches_by_country', + group: 'Football', + }, + { + label: 'Football: Get Commentaries by Fixture', + id: 'football_get_commentaries_by_fixture', + group: 'Football', + }, + { + label: 'Football: Get Current Leagues by Team', + id: 'football_get_current_leagues_by_team', + group: 'Football', + }, + { + label: 'Football: Get Expected Lineups by Player', + id: 'football_get_expected_lineups_by_player', + group: 'Football', + }, + { + label: 'Football: Get Expected Lineups by Team', + id: 'football_get_expected_lineups_by_team', + group: 'Football', + }, + { + label: 'Football: Get Extended Team Squad', + id: 'football_get_extended_team_squad', group: 'Football', }, + { label: 'Football: Get Fixture by ID', id: 'football_get_fixture', group: 'Football' }, { - label: 'Get Football Fixtures by Date', + label: 'Football: Get Fixtures by Date', id: 'football_get_fixtures_by_date', group: 'Football', }, { - label: 'Get Football Fixtures by Date Range', + label: 'Football: Get Fixtures by Date Range', id: 'football_get_fixtures_by_date_range', group: 'Football', }, - { label: 'Get Football Fixture by ID', id: 'football_get_fixture', group: 'Football' }, - { label: 'Get Football Head to Head', id: 'football_get_head_to_head', group: 'Football' }, - { label: 'Get Football Leagues', id: 'football_get_leagues', group: 'Football' }, - { label: 'Get Football League by ID', id: 'football_get_league', group: 'Football' }, - { label: 'Search Football Teams', id: 'football_search_teams', group: 'Football' }, - { label: 'Get Football Team by ID', id: 'football_get_team', group: 'Football' }, - { label: 'Get Football Team Squad', id: 'football_get_team_squad', group: 'Football' }, - { label: 'Search Football Players', id: 'football_search_players', group: 'Football' }, - { label: 'Get Football Player by ID', id: 'football_get_player', group: 'Football' }, { - label: 'Get Football Standings by Season', - id: 'football_get_standings_by_season', - group: 'Football', + label: 'Football: Get Fixtures by Date Range for Team', + id: 'football_get_fixtures_by_date_range_for_team', + group: 'Football', + }, + { + label: 'Football: Get Fixtures by Multiple IDs', + id: 'football_get_fixtures_by_ids', + group: 'Football', + }, + { + label: 'Football: Get Grouped Standings by Round', + id: 'football_get_grouped_standings_by_round', + group: 'Football', + }, + { label: 'Football: Get Head to Head', id: 'football_get_head_to_head', group: 'Football' }, + { + label: 'Football: Get Inplay Livescores', + id: 'football_get_inplay_livescores', + group: 'Football', + }, + { + label: 'Football: Get Last Updated Coaches', + id: 'football_get_latest_coaches', + group: 'Football', + }, + { + label: 'Football: Get Latest Updated Fixtures', + id: 'football_get_latest_fixtures', + group: 'Football', + }, + { + label: 'Football: Get Latest Updated Livescores', + id: 'football_get_latest_livescores', + group: 'Football', + }, + { + label: 'Football: Get Last Updated Players', + id: 'football_get_latest_players', + group: 'Football', + }, + { + label: 'Football: Get Latest Team of the Week', + id: 'football_get_latest_totw', + group: 'Football', + }, + { + label: 'Football: Get Latest Transfers', + id: 'football_get_latest_transfers', + group: 'Football', + }, + { label: 'Football: Get League by ID', id: 'football_get_league', group: 'Football' }, + { label: 'Football: Get Leagues', id: 'football_get_leagues', group: 'Football' }, + { + label: 'Football: Get Leagues by Country', + id: 'football_get_leagues_by_country', + group: 'Football', + }, + { + label: 'Football: Get Leagues by Date', + id: 'football_get_leagues_by_date', + group: 'Football', + }, + { + label: 'Football: Get Leagues by Team', + id: 'football_get_leagues_by_team', + group: 'Football', + }, + { label: 'Football: Get Live Leagues', id: 'football_get_live_leagues', group: 'Football' }, + { + label: 'Football: Get Live Probabilities', + id: 'football_get_live_probabilities', + group: 'Football', + }, + { + label: 'Football: Get Live Probabilities by Fixture', + id: 'football_get_live_probabilities_by_fixture', + group: 'Football', + }, + { + label: 'Football: Get Live Standings by League', + id: 'football_get_live_standings_by_league', + group: 'Football', + }, + { label: 'Football: Get Livescores', id: 'football_get_livescores', group: 'Football' }, + { + label: 'Football: Get All Match Facts', + id: 'football_get_match_facts', + group: 'Football', + }, + { + label: 'Football: Get Match Facts by Date Range', + id: 'football_get_match_facts_by_date_range', + group: 'Football', + }, + { + label: 'Football: Get Match Facts by Fixture', + id: 'football_get_match_facts_by_fixture', + group: 'Football', + }, + { + label: 'Football: Get Match Facts by League', + id: 'football_get_match_facts_by_league', + group: 'Football', + }, + { + label: 'Football: Get Past Fixtures by TV Station', + id: 'football_get_past_fixtures_by_tv_station', + group: 'Football', + }, + { label: 'Football: Get Player by ID', id: 'football_get_player', group: 'Football' }, + { + label: 'Football: Get Players by Country', + id: 'football_get_players_by_country', + group: 'Football', + }, + { + label: 'Football: Get Post-Match News', + id: 'football_get_postmatch_news', + group: 'Football', + }, + { + label: 'Football: Get Post-Match News by Season', + id: 'football_get_postmatch_news_by_season', + group: 'Football', + }, + { + label: 'Football: Get Predictability by League', + id: 'football_get_predictability_by_league', + group: 'Football', + }, + { + label: 'Football: Get Pre-Match News', + id: 'football_get_prematch_news', + group: 'Football', + }, + { + label: 'Football: Get Pre-Match News by Season', + id: 'football_get_prematch_news_by_season', + group: 'Football', + }, + { + label: 'Football: Get Pre-Match News for Upcoming Fixtures', + id: 'football_get_prematch_news_upcoming', + group: 'Football', + }, + { + label: 'Football: Get Probabilities', + id: 'football_get_probabilities', + group: 'Football', + }, + { + label: 'Football: Get Predictions by Fixture', + id: 'football_get_probabilities_by_fixture', + group: 'Football', + }, + { label: 'Football: Get Referee by ID', id: 'football_get_referee', group: 'Football' }, + { label: 'Football: Get Referees', id: 'football_get_referees', group: 'Football' }, + { + label: 'Football: Get Referees by Country', + id: 'football_get_referees_by_country', + group: 'Football', + }, + { + label: 'Football: Get Referees by Season', + id: 'football_get_referees_by_season', + group: 'Football', + }, + { + label: 'Football: Get Rivals by Team', + id: 'football_get_rivals_by_team', + group: 'Football', + }, + { label: 'Football: Get Round by ID', id: 'football_get_round', group: 'Football' }, + { + label: 'Football: Get Round Statistics', + id: 'football_get_round_statistics', + group: 'Football', + }, + { label: 'Football: Get Rounds', id: 'football_get_rounds', group: 'Football' }, + { + label: 'Football: Get Rounds by Season', + id: 'football_get_rounds_by_season', + group: 'Football', + }, + { + label: 'Football: Get Schedules by Season', + id: 'football_get_schedules_by_season', + group: 'Football', + }, + { + label: 'Football: Get Schedules by Season and Team', + id: 'football_get_schedules_by_season_and_team', + group: 'Football', + }, + { + label: 'Football: Get Schedules by Team', + id: 'football_get_schedules_by_team', + group: 'Football', + }, + { label: 'Football: Get Season by ID', id: 'football_get_season', group: 'Football' }, + { label: 'Football: Get Seasons', id: 'football_get_seasons', group: 'Football' }, + { + label: 'Football: Get Seasons by Team', + id: 'football_get_seasons_by_team', + group: 'Football', + }, + { label: 'Football: Get Stage by ID', id: 'football_get_stage', group: 'Football' }, + { + label: 'Football: Get Stage Statistics', + id: 'football_get_stage_statistics', + group: 'Football', + }, + { label: 'Football: Get Stages', id: 'football_get_stages', group: 'Football' }, + { + label: 'Football: Get Stages by Season', + id: 'football_get_stages_by_season', + group: 'Football', + }, + { + label: 'Football: Get Standing Corrections by Season', + id: 'football_get_standing_corrections_by_season', + group: 'Football', + }, + { label: 'Football: Get All Standings', id: 'football_get_standings', group: 'Football' }, + { + label: 'Football: Get Standings by Round', + id: 'football_get_standings_by_round', + group: 'Football', + }, + { + label: 'Football: Get Standings by Season', + id: 'football_get_standings_by_season', + group: 'Football', + }, + { label: 'Football: Get State by ID', id: 'football_get_state', group: 'Football' }, + { label: 'Football: Get States', id: 'football_get_states', group: 'Football' }, + { label: 'Football: Get Team by ID', id: 'football_get_team', group: 'Football' }, + { + label: 'Football: Get All Team Rankings', + id: 'football_get_team_rankings', + group: 'Football', + }, + { + label: 'Football: Get Team Rankings by Date', + id: 'football_get_team_rankings_by_date', + group: 'Football', + }, + { + label: 'Football: Get Team Rankings by Team', + id: 'football_get_team_rankings_by_team', + group: 'Football', + }, + { label: 'Football: Get Team Squad', id: 'football_get_team_squad', group: 'Football' }, + { + label: 'Football: Get Team Squad by Season', + id: 'football_get_team_squad_by_season', + group: 'Football', + }, + { + label: 'Football: Get Teams by Country', + id: 'football_get_teams_by_country', + group: 'Football', + }, + { + label: 'Football: Get Teams by Season', + id: 'football_get_teams_by_season', + group: 'Football', + }, + { + label: 'Football: Get Topscorers by Season', + id: 'football_get_topscorers_by_season', + group: 'Football', + }, + { + label: 'Football: Get Topscorers by Stage', + id: 'football_get_topscorers_by_stage', + group: 'Football', + }, + { label: 'Football: Get All Team of the Week', id: 'football_get_totw', group: 'Football' }, + { + label: 'Football: Get Team of the Week by Round', + id: 'football_get_totw_by_round', + group: 'Football', + }, + { label: 'Football: Get Transfer by ID', id: 'football_get_transfer', group: 'Football' }, + { + label: 'Football: Get Transfer Rumour by ID', + id: 'football_get_transfer_rumour', + group: 'Football', + }, + { + label: 'Football: Get Transfer Rumours Between Dates', + id: 'football_get_transfer_rumours_between_dates', + group: 'Football', + }, + { + label: 'Football: Get Transfer Rumours by Player', + id: 'football_get_transfer_rumours_by_player', + group: 'Football', + }, + { + label: 'Football: Get Transfer Rumours by Team', + id: 'football_get_transfer_rumours_by_team', + group: 'Football', + }, + { + label: 'Football: Get Transfers Between Dates', + id: 'football_get_transfers_between_dates', + group: 'Football', + }, + { + label: 'Football: Get Transfers by Player', + id: 'football_get_transfers_by_player', + group: 'Football', + }, + { + label: 'Football: Get Transfers by Team', + id: 'football_get_transfers_by_team', + group: 'Football', + }, + { + label: 'Football: Get TV Station by ID', + id: 'football_get_tv_station', + group: 'Football', + }, + { label: 'Football: Get TV Stations', id: 'football_get_tv_stations', group: 'Football' }, + { + label: 'Football: Get TV Stations by Fixture', + id: 'football_get_tv_stations_by_fixture', + group: 'Football', + }, + { + label: 'Football: Get Upcoming Fixtures by Market', + id: 'football_get_upcoming_fixtures_by_market', + group: 'Football', + }, + { + label: 'Football: Get Upcoming Fixtures by TV Station', + id: 'football_get_upcoming_fixtures_by_tv_station', + group: 'Football', + }, + { label: 'Football: Get Value Bets', id: 'football_get_value_bets', group: 'Football' }, + { + label: 'Football: Get Value Bets by Fixture', + id: 'football_get_value_bets_by_fixture', + group: 'Football', + }, + { label: 'Football: Get Venue by ID', id: 'football_get_venue', group: 'Football' }, + { label: 'Football: Get Venues', id: 'football_get_venues', group: 'Football' }, + { + label: 'Football: Get Venues by Season', + id: 'football_get_venues_by_season', + group: 'Football', + }, + { label: 'Football: Search Coaches', id: 'football_search_coaches', group: 'Football' }, + { label: 'Football: Search Fixtures', id: 'football_search_fixtures', group: 'Football' }, + { label: 'Football: Search Leagues', id: 'football_search_leagues', group: 'Football' }, + { label: 'Football: Search Players', id: 'football_search_players', group: 'Football' }, + { label: 'Football: Search Referees', id: 'football_search_referees', group: 'Football' }, + { label: 'Football: Search Rounds', id: 'football_search_rounds', group: 'Football' }, + { label: 'Football: Search Seasons', id: 'football_search_seasons', group: 'Football' }, + { label: 'Football: Search Stages', id: 'football_search_stages', group: 'Football' }, + { label: 'Football: Search Teams', id: 'football_search_teams', group: 'Football' }, + { label: 'Football: Search Venues', id: 'football_search_venues', group: 'Football' }, + { + label: 'Motorsport: Get All Fixtures', + id: 'motorsport_get_all_fixtures', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Current Leagues by Team', + id: 'motorsport_get_current_leagues_by_team', + group: 'Motorsport', + }, + { label: 'Motorsport: Get Driver by ID', id: 'motorsport_get_driver', group: 'Motorsport' }, + { + label: 'Motorsport: Get All Driver Standings', + id: 'motorsport_get_driver_standings', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Driver Standings by Season', + id: 'motorsport_get_driver_standings_by_season', + group: 'Motorsport', + }, + { label: 'Motorsport: Get Drivers', id: 'motorsport_get_drivers', group: 'Motorsport' }, + { + label: 'Motorsport: Get Drivers by Country', + id: 'motorsport_get_drivers_by_country', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Drivers by Season', + id: 'motorsport_get_drivers_by_season', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Fixture by ID', + id: 'motorsport_get_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Fixtures by Date', + id: 'motorsport_get_fixtures_by_date', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Fixtures by Date Range', + id: 'motorsport_get_fixtures_by_date_range', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Fixtures by IDs', + id: 'motorsport_get_fixtures_by_ids', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Laps by Fixture', + id: 'motorsport_get_laps_by_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Laps by Fixture and Driver', + id: 'motorsport_get_laps_by_fixture_and_driver', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Laps by Fixture and Lap Number', + id: 'motorsport_get_laps_by_fixture_and_lap', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Latest Laps by Fixture', + id: 'motorsport_get_latest_laps_by_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Latest Pitstops by Fixture', + id: 'motorsport_get_latest_pitstops_by_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Latest Stints by Fixture', + id: 'motorsport_get_latest_stints_by_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Latest Updated Drivers', + id: 'motorsport_get_latest_updated_drivers', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Latest Updated Fixtures', + id: 'motorsport_get_latest_updated_fixtures', + group: 'Motorsport', + }, + { label: 'Motorsport: Get League by ID', id: 'motorsport_get_league', group: 'Motorsport' }, + { label: 'Motorsport: Get All Leagues', id: 'motorsport_get_leagues', group: 'Motorsport' }, + { + label: 'Motorsport: Get Leagues by Country', + id: 'motorsport_get_leagues_by_country', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Leagues by Fixture Date', + id: 'motorsport_get_leagues_by_date', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Leagues by Live', + id: 'motorsport_get_leagues_by_live', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Leagues by Team', + id: 'motorsport_get_leagues_by_team', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Livescores', + id: 'motorsport_get_livescores', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Pitstops by Fixture', + id: 'motorsport_get_pitstops_by_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Pitstops by Fixture and Driver', + id: 'motorsport_get_pitstops_by_fixture_and_driver', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Pitstops by Fixture and Lap Number', + id: 'motorsport_get_pitstops_by_fixture_and_lap', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Race Results by Season and Driver', + id: 'motorsport_get_race_results_by_season_and_driver', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Race Results by Season and Team', + id: 'motorsport_get_race_results_by_season_and_team', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Schedules by Season', + id: 'motorsport_get_schedules_by_season', + group: 'Motorsport', + }, + { label: 'Motorsport: Get Season by ID', id: 'motorsport_get_season', group: 'Motorsport' }, + { label: 'Motorsport: Get All Seasons', id: 'motorsport_get_seasons', group: 'Motorsport' }, + { label: 'Motorsport: Get Stage by ID', id: 'motorsport_get_stage', group: 'Motorsport' }, + { label: 'Motorsport: Get All Stages', id: 'motorsport_get_stages', group: 'Motorsport' }, + { + label: 'Motorsport: Get Stages by Season', + id: 'motorsport_get_stages_by_season', + group: 'Motorsport', + }, + { label: 'Motorsport: Get State by ID', id: 'motorsport_get_state', group: 'Motorsport' }, + { label: 'Motorsport: Get All States', id: 'motorsport_get_states', group: 'Motorsport' }, + { + label: 'Motorsport: Get Stints by Fixture', + id: 'motorsport_get_stints_by_fixture', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Stints by Fixture and Driver', + id: 'motorsport_get_stints_by_fixture_and_driver', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Stints by Fixture and Stint Number', + id: 'motorsport_get_stints_by_fixture_and_stint', + group: 'Motorsport', + }, + { label: 'Motorsport: Get Team by ID', id: 'motorsport_get_team', group: 'Motorsport' }, + { + label: 'Motorsport: Get All Team Standings', + id: 'motorsport_get_team_standings', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Team Standings by Season', + id: 'motorsport_get_team_standings_by_season', + group: 'Motorsport', + }, + { label: 'Motorsport: Get Teams', id: 'motorsport_get_teams', group: 'Motorsport' }, + { + label: 'Motorsport: Get Teams by Country', + id: 'motorsport_get_teams_by_country', + group: 'Motorsport', + }, + { + label: 'Motorsport: Get Teams by Season', + id: 'motorsport_get_teams_by_season', + group: 'Motorsport', + }, + { label: 'Motorsport: Get Venue by ID', id: 'motorsport_get_venue', group: 'Motorsport' }, + { label: 'Motorsport: Get Venues', id: 'motorsport_get_venues', group: 'Motorsport' }, + { + label: 'Motorsport: Get Venues by Season', + id: 'motorsport_get_venues_by_season', + group: 'Motorsport', + }, + { + label: 'Motorsport: Search Drivers', + id: 'motorsport_search_drivers', + group: 'Motorsport', + }, + { + label: 'Motorsport: Search Leagues', + id: 'motorsport_search_leagues', + group: 'Motorsport', + }, + { label: 'Motorsport: Search Stages', id: 'motorsport_search_stages', group: 'Motorsport' }, + { label: 'Motorsport: Search Teams', id: 'motorsport_search_teams', group: 'Motorsport' }, + { label: 'Motorsport: Search Venues', id: 'motorsport_search_venues', group: 'Motorsport' }, + { + label: 'Odds: Get All Historical Odds', + id: 'odds_get_all_historical_odds', + group: 'Odds', + }, + { label: 'Odds: Get All In-play Odds', id: 'odds_get_all_inplay_odds', group: 'Odds' }, + { label: 'Odds: Get All Pre-match Odds', id: 'odds_get_all_pre_match_odds', group: 'Odds' }, + { label: 'Odds: Get All Premium Odds', id: 'odds_get_all_premium_odds', group: 'Odds' }, + { label: 'Odds: Get Bookmaker by ID', id: 'odds_get_bookmaker', group: 'Odds' }, + { + label: 'Odds: Get Bookmaker Event IDs by Fixture', + id: 'odds_get_bookmaker_event_ids_by_fixture', + group: 'Odds', + }, + { label: 'Odds: Get Bookmakers', id: 'odds_get_bookmakers', group: 'Odds' }, + { + label: 'Odds: Get Bookmakers by Fixture', + id: 'odds_get_bookmakers_by_fixture', + group: 'Odds', + }, + { + label: 'Odds: Get In-play Odds by Fixture', + id: 'odds_get_inplay_odds_by_fixture', + group: 'Odds', + }, + { + label: 'Odds: Get In-play Odds by Fixture and Bookmaker', + id: 'odds_get_inplay_odds_by_fixture_and_bookmaker', + group: 'Odds', + }, + { + label: 'Odds: Get In-play Odds by Fixture and Market', + id: 'odds_get_inplay_odds_by_fixture_and_market', + group: 'Odds', }, { - label: 'Get Football Topscorers by Season', - id: 'football_get_topscorers_by_season', - group: 'Football', + label: 'Odds: Get Last Updated In-play Odds', + id: 'odds_get_last_updated_inplay_odds', + group: 'Odds', }, - // Motorsport { - label: 'Get Live Motorsport Scores', - id: 'motorsport_get_livescores', - group: 'Motorsport', + label: 'Odds: Get Last Updated Pre-match Odds', + id: 'odds_get_last_updated_pre_match_odds', + group: 'Odds', }, + { label: 'Odds: Get Market by ID', id: 'odds_get_market', group: 'Odds' }, + { label: 'Odds: Get Markets', id: 'odds_get_markets', group: 'Odds' }, { - label: 'Get Motorsport Fixtures by Date', - id: 'motorsport_get_fixtures_by_date', - group: 'Motorsport', + label: 'Odds: Get Pre-match Odds by Fixture', + id: 'odds_get_pre_match_odds_by_fixture', + group: 'Odds', }, { - label: 'Get Motorsport Fixture by ID', - id: 'motorsport_get_fixture', - group: 'Motorsport', + label: 'Odds: Get Pre-match Odds by Fixture and Bookmaker', + id: 'odds_get_pre_match_odds_by_fixture_and_bookmaker', + group: 'Odds', }, - { label: 'Get Motorsport Drivers', id: 'motorsport_get_drivers', group: 'Motorsport' }, - { label: 'Get Motorsport Driver by ID', id: 'motorsport_get_driver', group: 'Motorsport' }, { - label: 'Search Motorsport Drivers', - id: 'motorsport_search_drivers', - group: 'Motorsport', + label: 'Odds: Get Pre-match Odds by Fixture and Market', + id: 'odds_get_pre_match_odds_by_fixture_and_market', + group: 'Odds', }, - { label: 'Get Motorsport Teams', id: 'motorsport_get_teams', group: 'Motorsport' }, - { label: 'Get Motorsport Team by ID', id: 'motorsport_get_team', group: 'Motorsport' }, { - label: 'Get Motorsport Driver Standings by Season', - id: 'motorsport_get_driver_standings_by_season', - group: 'Motorsport', + label: 'Odds: Get Premium Odds by Fixture', + id: 'odds_get_premium_odds_by_fixture', + group: 'Odds', }, { - label: 'Get Motorsport Team Standings by Season', - id: 'motorsport_get_team_standings_by_season', - group: 'Motorsport', + label: 'Odds: Get Premium Odds by Fixture and Bookmaker', + id: 'odds_get_premium_odds_by_fixture_and_bookmaker', + group: 'Odds', }, { - label: 'Get Motorsport Laps by Fixture', - id: 'motorsport_get_laps_by_fixture', - group: 'Motorsport', + label: 'Odds: Get Premium Odds by Fixture and Market', + id: 'odds_get_premium_odds_by_fixture_and_market', + group: 'Odds', }, { - label: 'Get Motorsport Pitstops by Fixture', - id: 'motorsport_get_pitstops_by_fixture', - group: 'Motorsport', + label: 'Odds: Get Updated Historical Odds Between Time Range', + id: 'odds_get_updated_historical_odds_between', + group: 'Odds', }, - // Odds { - label: 'Get Pre-match Odds by Fixture', - id: 'odds_get_pre_match_odds_by_fixture', + label: 'Odds: Get Updated Premium Odds Between Time Range', + id: 'odds_get_updated_premium_odds_between', group: 'Odds', }, + { label: 'Odds: Search Bookmakers', id: 'odds_search_bookmakers', group: 'Odds' }, + { label: 'Odds: Search Markets', id: 'odds_search_markets', group: 'Odds' }, + { label: 'Core: Get Cities', id: 'core_get_cities', group: 'Core (Reference)' }, + { label: 'Core: Get City by ID', id: 'core_get_city', group: 'Core (Reference)' }, + { label: 'Core: Get Continent by ID', id: 'core_get_continent', group: 'Core (Reference)' }, + { label: 'Core: Get Continents', id: 'core_get_continents', group: 'Core (Reference)' }, + { label: 'Core: Get Countries', id: 'core_get_countries', group: 'Core (Reference)' }, + { label: 'Core: Get Country by ID', id: 'core_get_country', group: 'Core (Reference)' }, { - label: 'Get In-play Odds by Fixture', - id: 'odds_get_inplay_odds_by_fixture', - group: 'Odds', + label: 'Core: Get All Entity Filters', + id: 'core_get_entity_filters', + group: 'Core (Reference)', + }, + { label: 'Core: Get My Usage', id: 'core_get_my_usage', group: 'Core (Reference)' }, + { label: 'Core: Get Region by ID', id: 'core_get_region', group: 'Core (Reference)' }, + { label: 'Core: Get Regions', id: 'core_get_regions', group: 'Core (Reference)' }, + { label: 'Core: Get Timezones', id: 'core_get_timezones', group: 'Core (Reference)' }, + { label: 'Core: Get Type by ID', id: 'core_get_type', group: 'Core (Reference)' }, + { + label: 'Core: Get Type by Entity', + id: 'core_get_type_by_entity', + group: 'Core (Reference)', }, - { label: 'Get Bookmakers', id: 'odds_get_bookmakers', group: 'Odds' }, - { label: 'Get Bookmaker by ID', id: 'odds_get_bookmaker', group: 'Odds' }, - { label: 'Search Bookmakers', id: 'odds_search_bookmakers', group: 'Odds' }, - { label: 'Get Betting Markets', id: 'odds_get_markets', group: 'Odds' }, - { label: 'Get Betting Market by ID', id: 'odds_get_market', group: 'Odds' }, - { label: 'Search Betting Markets', id: 'odds_search_markets', group: 'Odds' }, - // Core reference data - { label: 'Get Continents', id: 'core_get_continents', group: 'Core (Reference)' }, - { label: 'Get Continent by ID', id: 'core_get_continent', group: 'Core (Reference)' }, - { label: 'Get Countries', id: 'core_get_countries', group: 'Core (Reference)' }, - { label: 'Get Country by ID', id: 'core_get_country', group: 'Core (Reference)' }, - { label: 'Search Countries', id: 'core_search_countries', group: 'Core (Reference)' }, - { label: 'Get Regions', id: 'core_get_regions', group: 'Core (Reference)' }, - { label: 'Get Region by ID', id: 'core_get_region', group: 'Core (Reference)' }, - { label: 'Get Cities', id: 'core_get_cities', group: 'Core (Reference)' }, - { label: 'Get City by ID', id: 'core_get_city', group: 'Core (Reference)' }, - { label: 'Search Cities', id: 'core_search_cities', group: 'Core (Reference)' }, - { label: 'Get Types', id: 'core_get_types', group: 'Core (Reference)' }, - { label: 'Get Type by ID', id: 'core_get_type', group: 'Core (Reference)' }, - { label: 'Get Timezones', id: 'core_get_timezones', group: 'Core (Reference)' }, + { label: 'Core: Get Types', id: 'core_get_types', group: 'Core (Reference)' }, + { label: 'Core: Search Cities', id: 'core_search_cities', group: 'Core (Reference)' }, + { label: 'Core: Search Countries', id: 'core_search_countries', group: 'Core (Reference)' }, + { label: 'Core: Search Regions', id: 'core_search_regions', group: 'Core (Reference)' }, ], value: () => 'football_get_fixtures_by_date', }, - // Date inputs (football + motorsport fixtures by date) + { + id: 'bookmakerId', + title: 'Bookmaker ID', + type: 'short-input', + placeholder: 'Numeric bookmaker ID', + condition: { field: 'operation', value: BOOKMAKER_ID_OPS }, + required: { field: 'operation', value: BOOKMAKER_ID_OPS }, + }, + { + id: 'cityId', + title: 'City ID', + type: 'short-input', + placeholder: 'Numeric city ID', + condition: { field: 'operation', value: CITY_ID_OPS }, + required: { field: 'operation', value: CITY_ID_OPS }, + }, + { + id: 'coachId', + title: 'Coach ID', + type: 'short-input', + placeholder: 'Numeric coach ID', + condition: { field: 'operation', value: COACH_ID_OPS }, + required: { field: 'operation', value: COACH_ID_OPS }, + }, + { + id: 'continentId', + title: 'Continent ID', + type: 'short-input', + placeholder: 'Numeric continent ID', + condition: { field: 'operation', value: CONTINENT_ID_OPS }, + required: { field: 'operation', value: CONTINENT_ID_OPS }, + }, + { + id: 'countryId', + title: 'Country ID', + type: 'short-input', + placeholder: 'Numeric country ID', + condition: { field: 'operation', value: COUNTRY_ID_OPS }, + required: { field: 'operation', value: COUNTRY_ID_OPS }, + }, { id: 'date', title: 'Date', type: 'short-input', placeholder: 'YYYY-MM-DD', - condition: { - field: 'operation', - value: ['football_get_fixtures_by_date', 'motorsport_get_fixtures_by_date'], - }, - required: { - field: 'operation', - value: ['football_get_fixtures_by_date', 'motorsport_get_fixtures_by_date'], - }, wandConfig: DATE_WAND_CONFIG, + condition: { field: 'operation', value: DATE_OPS }, + required: { field: 'operation', value: DATE_OPS }, }, { - id: 'startDate', - title: 'Start Date', + id: 'driverId', + title: 'Driver ID', type: 'short-input', - placeholder: 'YYYY-MM-DD', - condition: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, - required: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, - wandConfig: DATE_WAND_CONFIG, + placeholder: 'Numeric driver ID', + condition: { field: 'operation', value: DRIVER_ID_OPS }, + required: { field: 'operation', value: DRIVER_ID_OPS }, }, { id: 'endDate', title: 'End Date', type: 'short-input', - placeholder: 'YYYY-MM-DD (max 100 days after start)', - condition: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, - required: { field: 'operation', value: 'football_get_fixtures_by_date_range' }, + placeholder: 'YYYY-MM-DD', wandConfig: DATE_WAND_CONFIG, + condition: { field: 'operation', value: END_DATE_OPS }, + required: { field: 'operation', value: END_DATE_OPS }, }, - // Fixture ID (football + motorsport + odds fixture operations) { id: 'fixtureId', title: 'Fixture ID', type: 'short-input', placeholder: 'Numeric fixture ID', - condition: { - field: 'operation', - value: [ - 'football_get_fixture', - 'motorsport_get_fixture', - 'motorsport_get_laps_by_fixture', - 'motorsport_get_pitstops_by_fixture', - 'odds_get_pre_match_odds_by_fixture', - 'odds_get_inplay_odds_by_fixture', - ], - }, - required: { - field: 'operation', - value: [ - 'football_get_fixture', - 'motorsport_get_fixture', - 'motorsport_get_laps_by_fixture', - 'motorsport_get_pitstops_by_fixture', - 'odds_get_pre_match_odds_by_fixture', - 'odds_get_inplay_odds_by_fixture', - ], - }, + condition: { field: 'operation', value: FIXTURE_ID_OPS }, + required: { field: 'operation', value: FIXTURE_ID_OPS }, }, - // Head to head team IDs (football) { - id: 'team1', - title: 'Team 1 ID', + id: 'fixtureIds', + title: 'Fixture IDs', type: 'short-input', - placeholder: 'First team ID', - condition: { field: 'operation', value: 'football_get_head_to_head' }, - required: { field: 'operation', value: 'football_get_head_to_head' }, + placeholder: 'Comma-separated fixture IDs', + condition: { field: 'operation', value: FIXTURE_IDS_OPS }, + required: { field: 'operation', value: FIXTURE_IDS_OPS }, }, { - id: 'team2', - title: 'Team 2 ID', + id: 'fromTimestamp', + title: 'From (UNIX timestamp)', type: 'short-input', - placeholder: 'Second team ID', - condition: { field: 'operation', value: 'football_get_head_to_head' }, - required: { field: 'operation', value: 'football_get_head_to_head' }, + placeholder: 'Start UNIX timestamp', + condition: { field: 'operation', value: FROM_TIMESTAMP_OPS }, + required: { field: 'operation', value: FROM_TIMESTAMP_OPS }, }, - // League ID (football) { - id: 'leagueId', - title: 'League ID', + id: 'ids', + title: 'IDs', type: 'short-input', - placeholder: 'Numeric league ID', - condition: { field: 'operation', value: 'football_get_league' }, - required: { field: 'operation', value: 'football_get_league' }, + placeholder: 'Comma-separated IDs', + condition: { field: 'operation', value: IDS_OPS }, + required: { field: 'operation', value: IDS_OPS }, }, - // Team ID (football + motorsport) { - id: 'teamId', - title: 'Team ID', + id: 'lapNumber', + title: 'Lap Number', type: 'short-input', - placeholder: 'Numeric team ID', - condition: { - field: 'operation', - value: ['football_get_team', 'football_get_team_squad', 'motorsport_get_team'], - }, - required: { - field: 'operation', - value: ['football_get_team', 'football_get_team_squad', 'motorsport_get_team'], - }, + placeholder: 'Numeric lap number', + condition: { field: 'operation', value: LAP_NUMBER_OPS }, + required: { field: 'operation', value: LAP_NUMBER_OPS }, }, - // Driver ID (motorsport) { - id: 'driverId', - title: 'Driver ID', + id: 'leagueId', + title: 'League ID', type: 'short-input', - placeholder: 'Numeric driver ID', - condition: { field: 'operation', value: 'motorsport_get_driver' }, - required: { field: 'operation', value: 'motorsport_get_driver' }, + placeholder: 'Numeric league ID', + condition: { field: 'operation', value: LEAGUE_ID_OPS }, + required: { field: 'operation', value: LEAGUE_ID_OPS }, + }, + { + id: 'marketId', + title: 'Market ID', + type: 'short-input', + placeholder: 'Numeric market ID', + condition: { field: 'operation', value: MARKET_ID_OPS }, + required: { field: 'operation', value: MARKET_ID_OPS }, }, - // Player ID (football) { id: 'playerId', title: 'Player ID', type: 'short-input', placeholder: 'Numeric player ID', - condition: { field: 'operation', value: 'football_get_player' }, - required: { field: 'operation', value: 'football_get_player' }, + condition: { field: 'operation', value: PLAYER_ID_OPS }, + required: { field: 'operation', value: PLAYER_ID_OPS }, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Name to search for', + condition: { field: 'operation', value: QUERY_OPS }, + required: { field: 'operation', value: QUERY_OPS }, + }, + { + id: 'refereeId', + title: 'Referee ID', + type: 'short-input', + placeholder: 'Numeric referee ID', + condition: { field: 'operation', value: REFEREE_ID_OPS }, + required: { field: 'operation', value: REFEREE_ID_OPS }, + }, + { + id: 'regionId', + title: 'Region ID', + type: 'short-input', + placeholder: 'Numeric region ID', + condition: { field: 'operation', value: REGION_ID_OPS }, + required: { field: 'operation', value: REGION_ID_OPS }, + }, + { + id: 'roundId', + title: 'Round ID', + type: 'short-input', + placeholder: 'Numeric round ID', + condition: { field: 'operation', value: ROUND_ID_OPS }, + required: { field: 'operation', value: ROUND_ID_OPS }, + }, + { + id: 'rumourId', + title: 'Rumour ID', + type: 'short-input', + placeholder: 'Numeric transfer rumour ID', + condition: { field: 'operation', value: RUMOUR_ID_OPS }, + required: { field: 'operation', value: RUMOUR_ID_OPS }, }, - // Season ID (football + motorsport standings/topscorers) { id: 'seasonId', title: 'Season ID', type: 'short-input', placeholder: 'Numeric season ID', - condition: { - field: 'operation', - value: [ - 'football_get_standings_by_season', - 'football_get_topscorers_by_season', - 'motorsport_get_driver_standings_by_season', - 'motorsport_get_team_standings_by_season', - ], - }, - required: { - field: 'operation', - value: [ - 'football_get_standings_by_season', - 'football_get_topscorers_by_season', - 'motorsport_get_driver_standings_by_season', - 'motorsport_get_team_standings_by_season', - ], - }, + condition: { field: 'operation', value: SEASON_ID_OPS }, + required: { field: 'operation', value: SEASON_ID_OPS }, }, - // Bookmaker / Market IDs (odds) { - id: 'bookmakerId', - title: 'Bookmaker ID', + id: 'stageId', + title: 'Stage ID', type: 'short-input', - placeholder: 'Numeric bookmaker ID', - condition: { field: 'operation', value: 'odds_get_bookmaker' }, - required: { field: 'operation', value: 'odds_get_bookmaker' }, + placeholder: 'Numeric stage ID', + condition: { field: 'operation', value: STAGE_ID_OPS }, + required: { field: 'operation', value: STAGE_ID_OPS }, }, { - id: 'marketId', - title: 'Market ID', + id: 'startDate', + title: 'Start Date', type: 'short-input', - placeholder: 'Numeric market ID', - condition: { field: 'operation', value: 'odds_get_market' }, - required: { field: 'operation', value: 'odds_get_market' }, + placeholder: 'YYYY-MM-DD', + wandConfig: DATE_WAND_CONFIG, + condition: { field: 'operation', value: START_DATE_OPS }, + required: { field: 'operation', value: START_DATE_OPS }, }, - // Core reference IDs { - id: 'continentId', - title: 'Continent ID', + id: 'stateId', + title: 'State ID', type: 'short-input', - placeholder: 'Numeric continent ID', - condition: { field: 'operation', value: 'core_get_continent' }, - required: { field: 'operation', value: 'core_get_continent' }, + placeholder: 'Numeric state ID', + condition: { field: 'operation', value: STATE_ID_OPS }, + required: { field: 'operation', value: STATE_ID_OPS }, }, { - id: 'countryId', - title: 'Country ID', + id: 'stintNumber', + title: 'Stint Number', type: 'short-input', - placeholder: 'Numeric country ID', - condition: { field: 'operation', value: 'core_get_country' }, - required: { field: 'operation', value: 'core_get_country' }, + placeholder: 'Numeric stint number', + condition: { field: 'operation', value: STINT_NUMBER_OPS }, + required: { field: 'operation', value: STINT_NUMBER_OPS }, }, { - id: 'regionId', - title: 'Region ID', + id: 'team1', + title: 'Team 1 ID', type: 'short-input', - placeholder: 'Numeric region ID', - condition: { field: 'operation', value: 'core_get_region' }, - required: { field: 'operation', value: 'core_get_region' }, + placeholder: 'First team ID', + condition: { field: 'operation', value: TEAM_1_OPS }, + required: { field: 'operation', value: TEAM_1_OPS }, }, { - id: 'cityId', - title: 'City ID', + id: 'team2', + title: 'Team 2 ID', type: 'short-input', - placeholder: 'Numeric city ID', - condition: { field: 'operation', value: 'core_get_city' }, - required: { field: 'operation', value: 'core_get_city' }, + placeholder: 'Second team ID', + condition: { field: 'operation', value: TEAM_2_OPS }, + required: { field: 'operation', value: TEAM_2_OPS }, + }, + { + id: 'teamId', + title: 'Team ID', + type: 'short-input', + placeholder: 'Numeric team ID', + condition: { field: 'operation', value: TEAM_ID_OPS }, + required: { field: 'operation', value: TEAM_ID_OPS }, + }, + { + id: 'toTimestamp', + title: 'To (UNIX timestamp)', + type: 'short-input', + placeholder: 'End UNIX timestamp', + condition: { field: 'operation', value: TO_TIMESTAMP_OPS }, + required: { field: 'operation', value: TO_TIMESTAMP_OPS }, + }, + { + id: 'transferId', + title: 'Transfer ID', + type: 'short-input', + placeholder: 'Numeric transfer ID', + condition: { field: 'operation', value: TRANSFER_ID_OPS }, + required: { field: 'operation', value: TRANSFER_ID_OPS }, + }, + { + id: 'tvStationId', + title: 'TV Station ID', + type: 'short-input', + placeholder: 'Numeric TV station ID', + condition: { field: 'operation', value: TV_STATION_ID_OPS }, + required: { field: 'operation', value: TV_STATION_ID_OPS }, }, { id: 'typeId', title: 'Type ID', type: 'short-input', placeholder: 'Numeric type ID', - condition: { field: 'operation', value: 'core_get_type' }, - required: { field: 'operation', value: 'core_get_type' }, + condition: { field: 'operation', value: TYPE_ID_OPS }, + required: { field: 'operation', value: TYPE_ID_OPS }, }, - // Shared search query { - id: 'query', - title: 'Search Query', + id: 'venueId', + title: 'Venue ID', type: 'short-input', - placeholder: 'Name to search for', - condition: { - field: 'operation', - value: [ - 'football_search_teams', - 'football_search_players', - 'motorsport_search_drivers', - 'odds_search_bookmakers', - 'odds_search_markets', - 'core_search_countries', - 'core_search_cities', - ], - }, - required: { - field: 'operation', - value: [ - 'football_search_teams', - 'football_search_players', - 'motorsport_search_drivers', - 'odds_search_bookmakers', - 'odds_search_markets', - 'core_search_countries', - 'core_search_cities', - ], - }, + placeholder: 'Numeric venue ID', + condition: { field: 'operation', value: VENUE_ID_OPS }, + required: { field: 'operation', value: VENUE_ID_OPS }, }, - // Shared enrichment + pagination (advanced) { id: 'include', title: 'Includes', @@ -529,54 +1925,227 @@ export const SportmonksBlock: BlockConfig = { ], tools: { access: [ - 'sportmonks_football_get_livescores', - 'sportmonks_football_get_inplay_livescores', + 'sportmonks_football_expected_by_player', + 'sportmonks_football_expected_by_team', + 'sportmonks_football_get_all_commentaries', + 'sportmonks_football_get_all_fixtures', + 'sportmonks_football_get_all_players', + 'sportmonks_football_get_all_rivals', + 'sportmonks_football_get_all_teams', + 'sportmonks_football_get_all_transfer_rumours', + 'sportmonks_football_get_all_transfers', + 'sportmonks_football_get_brackets_by_season', + 'sportmonks_football_get_coach', + 'sportmonks_football_get_coaches', + 'sportmonks_football_get_coaches_by_country', + 'sportmonks_football_get_commentaries_by_fixture', + 'sportmonks_football_get_current_leagues_by_team', + 'sportmonks_football_get_expected_lineups_by_player', + 'sportmonks_football_get_expected_lineups_by_team', + 'sportmonks_football_get_extended_team_squad', + 'sportmonks_football_get_fixture', 'sportmonks_football_get_fixtures_by_date', 'sportmonks_football_get_fixtures_by_date_range', - 'sportmonks_football_get_fixture', + 'sportmonks_football_get_fixtures_by_date_range_for_team', + 'sportmonks_football_get_fixtures_by_ids', + 'sportmonks_football_get_grouped_standings_by_round', 'sportmonks_football_get_head_to_head', - 'sportmonks_football_get_leagues', + 'sportmonks_football_get_inplay_livescores', + 'sportmonks_football_get_latest_coaches', + 'sportmonks_football_get_latest_fixtures', + 'sportmonks_football_get_latest_livescores', + 'sportmonks_football_get_latest_players', + 'sportmonks_football_get_latest_totw', + 'sportmonks_football_get_latest_transfers', 'sportmonks_football_get_league', - 'sportmonks_football_search_teams', - 'sportmonks_football_get_team', - 'sportmonks_football_get_team_squad', - 'sportmonks_football_search_players', + 'sportmonks_football_get_leagues', + 'sportmonks_football_get_leagues_by_country', + 'sportmonks_football_get_leagues_by_date', + 'sportmonks_football_get_leagues_by_team', + 'sportmonks_football_get_live_leagues', + 'sportmonks_football_get_live_probabilities', + 'sportmonks_football_get_live_probabilities_by_fixture', + 'sportmonks_football_get_live_standings_by_league', + 'sportmonks_football_get_livescores', + 'sportmonks_football_get_match_facts', + 'sportmonks_football_get_match_facts_by_date_range', + 'sportmonks_football_get_match_facts_by_fixture', + 'sportmonks_football_get_match_facts_by_league', + 'sportmonks_football_get_past_fixtures_by_tv_station', 'sportmonks_football_get_player', + 'sportmonks_football_get_players_by_country', + 'sportmonks_football_get_postmatch_news', + 'sportmonks_football_get_postmatch_news_by_season', + 'sportmonks_football_get_predictability_by_league', + 'sportmonks_football_get_prematch_news', + 'sportmonks_football_get_prematch_news_by_season', + 'sportmonks_football_get_prematch_news_upcoming', + 'sportmonks_football_get_probabilities', + 'sportmonks_football_get_probabilities_by_fixture', + 'sportmonks_football_get_referee', + 'sportmonks_football_get_referees', + 'sportmonks_football_get_referees_by_country', + 'sportmonks_football_get_referees_by_season', + 'sportmonks_football_get_rivals_by_team', + 'sportmonks_football_get_round', + 'sportmonks_football_get_round_statistics', + 'sportmonks_football_get_rounds', + 'sportmonks_football_get_rounds_by_season', + 'sportmonks_football_get_schedules_by_season', + 'sportmonks_football_get_schedules_by_season_and_team', + 'sportmonks_football_get_schedules_by_team', + 'sportmonks_football_get_season', + 'sportmonks_football_get_seasons', + 'sportmonks_football_get_seasons_by_team', + 'sportmonks_football_get_stage', + 'sportmonks_football_get_stage_statistics', + 'sportmonks_football_get_stages', + 'sportmonks_football_get_stages_by_season', + 'sportmonks_football_get_standing_corrections_by_season', + 'sportmonks_football_get_standings', + 'sportmonks_football_get_standings_by_round', 'sportmonks_football_get_standings_by_season', + 'sportmonks_football_get_state', + 'sportmonks_football_get_states', + 'sportmonks_football_get_team', + 'sportmonks_football_get_team_rankings', + 'sportmonks_football_get_team_rankings_by_date', + 'sportmonks_football_get_team_rankings_by_team', + 'sportmonks_football_get_team_squad', + 'sportmonks_football_get_team_squad_by_season', + 'sportmonks_football_get_teams_by_country', + 'sportmonks_football_get_teams_by_season', 'sportmonks_football_get_topscorers_by_season', - 'sportmonks_motorsport_get_livescores', - 'sportmonks_motorsport_get_fixtures_by_date', - 'sportmonks_motorsport_get_fixture', - 'sportmonks_motorsport_get_drivers', + 'sportmonks_football_get_topscorers_by_stage', + 'sportmonks_football_get_totw', + 'sportmonks_football_get_totw_by_round', + 'sportmonks_football_get_transfer', + 'sportmonks_football_get_transfer_rumour', + 'sportmonks_football_get_transfer_rumours_between_dates', + 'sportmonks_football_get_transfer_rumours_by_player', + 'sportmonks_football_get_transfer_rumours_by_team', + 'sportmonks_football_get_transfers_between_dates', + 'sportmonks_football_get_transfers_by_player', + 'sportmonks_football_get_transfers_by_team', + 'sportmonks_football_get_tv_station', + 'sportmonks_football_get_tv_stations', + 'sportmonks_football_get_tv_stations_by_fixture', + 'sportmonks_football_get_upcoming_fixtures_by_market', + 'sportmonks_football_get_upcoming_fixtures_by_tv_station', + 'sportmonks_football_get_value_bets', + 'sportmonks_football_get_value_bets_by_fixture', + 'sportmonks_football_get_venue', + 'sportmonks_football_get_venues', + 'sportmonks_football_get_venues_by_season', + 'sportmonks_football_search_coaches', + 'sportmonks_football_search_fixtures', + 'sportmonks_football_search_leagues', + 'sportmonks_football_search_players', + 'sportmonks_football_search_referees', + 'sportmonks_football_search_rounds', + 'sportmonks_football_search_seasons', + 'sportmonks_football_search_stages', + 'sportmonks_football_search_teams', + 'sportmonks_football_search_venues', + 'sportmonks_motorsport_get_all_fixtures', + 'sportmonks_motorsport_get_current_leagues_by_team', 'sportmonks_motorsport_get_driver', - 'sportmonks_motorsport_search_drivers', - 'sportmonks_motorsport_get_teams', - 'sportmonks_motorsport_get_team', + 'sportmonks_motorsport_get_driver_standings', 'sportmonks_motorsport_get_driver_standings_by_season', - 'sportmonks_motorsport_get_team_standings_by_season', + 'sportmonks_motorsport_get_drivers', + 'sportmonks_motorsport_get_drivers_by_country', + 'sportmonks_motorsport_get_drivers_by_season', + 'sportmonks_motorsport_get_fixture', + 'sportmonks_motorsport_get_fixtures_by_date', + 'sportmonks_motorsport_get_fixtures_by_date_range', + 'sportmonks_motorsport_get_fixtures_by_ids', 'sportmonks_motorsport_get_laps_by_fixture', + 'sportmonks_motorsport_get_laps_by_fixture_and_driver', + 'sportmonks_motorsport_get_laps_by_fixture_and_lap', + 'sportmonks_motorsport_get_latest_laps_by_fixture', + 'sportmonks_motorsport_get_latest_pitstops_by_fixture', + 'sportmonks_motorsport_get_latest_stints_by_fixture', + 'sportmonks_motorsport_get_latest_updated_drivers', + 'sportmonks_motorsport_get_latest_updated_fixtures', + 'sportmonks_motorsport_get_league', + 'sportmonks_motorsport_get_leagues', + 'sportmonks_motorsport_get_leagues_by_country', + 'sportmonks_motorsport_get_leagues_by_date', + 'sportmonks_motorsport_get_leagues_by_live', + 'sportmonks_motorsport_get_leagues_by_team', + 'sportmonks_motorsport_get_livescores', 'sportmonks_motorsport_get_pitstops_by_fixture', - 'sportmonks_odds_get_pre_match_odds_by_fixture', - 'sportmonks_odds_get_inplay_odds_by_fixture', - 'sportmonks_odds_get_bookmakers', + 'sportmonks_motorsport_get_pitstops_by_fixture_and_driver', + 'sportmonks_motorsport_get_pitstops_by_fixture_and_lap', + 'sportmonks_motorsport_get_race_results_by_season_and_driver', + 'sportmonks_motorsport_get_race_results_by_season_and_team', + 'sportmonks_motorsport_get_schedules_by_season', + 'sportmonks_motorsport_get_season', + 'sportmonks_motorsport_get_seasons', + 'sportmonks_motorsport_get_stage', + 'sportmonks_motorsport_get_stages', + 'sportmonks_motorsport_get_stages_by_season', + 'sportmonks_motorsport_get_state', + 'sportmonks_motorsport_get_states', + 'sportmonks_motorsport_get_stints_by_fixture', + 'sportmonks_motorsport_get_stints_by_fixture_and_driver', + 'sportmonks_motorsport_get_stints_by_fixture_and_stint', + 'sportmonks_motorsport_get_team', + 'sportmonks_motorsport_get_team_standings', + 'sportmonks_motorsport_get_team_standings_by_season', + 'sportmonks_motorsport_get_teams', + 'sportmonks_motorsport_get_teams_by_country', + 'sportmonks_motorsport_get_teams_by_season', + 'sportmonks_motorsport_get_venue', + 'sportmonks_motorsport_get_venues', + 'sportmonks_motorsport_get_venues_by_season', + 'sportmonks_motorsport_search_drivers', + 'sportmonks_motorsport_search_leagues', + 'sportmonks_motorsport_search_stages', + 'sportmonks_motorsport_search_teams', + 'sportmonks_motorsport_search_venues', + 'sportmonks_odds_get_all_historical_odds', + 'sportmonks_odds_get_all_inplay_odds', + 'sportmonks_odds_get_all_pre_match_odds', + 'sportmonks_odds_get_all_premium_odds', 'sportmonks_odds_get_bookmaker', - 'sportmonks_odds_search_bookmakers', - 'sportmonks_odds_get_markets', + 'sportmonks_odds_get_bookmaker_event_ids_by_fixture', + 'sportmonks_odds_get_bookmakers', + 'sportmonks_odds_get_bookmakers_by_fixture', + 'sportmonks_odds_get_inplay_odds_by_fixture', + 'sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker', + 'sportmonks_odds_get_inplay_odds_by_fixture_and_market', + 'sportmonks_odds_get_last_updated_inplay_odds', + 'sportmonks_odds_get_last_updated_pre_match_odds', 'sportmonks_odds_get_market', + 'sportmonks_odds_get_markets', + 'sportmonks_odds_get_pre_match_odds_by_fixture', + 'sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker', + 'sportmonks_odds_get_pre_match_odds_by_fixture_and_market', + 'sportmonks_odds_get_premium_odds_by_fixture', + 'sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker', + 'sportmonks_odds_get_premium_odds_by_fixture_and_market', + 'sportmonks_odds_get_updated_historical_odds_between', + 'sportmonks_odds_get_updated_premium_odds_between', + 'sportmonks_odds_search_bookmakers', 'sportmonks_odds_search_markets', - 'sportmonks_core_get_continents', + 'sportmonks_core_get_cities', + 'sportmonks_core_get_city', 'sportmonks_core_get_continent', + 'sportmonks_core_get_continents', 'sportmonks_core_get_countries', 'sportmonks_core_get_country', - 'sportmonks_core_search_countries', - 'sportmonks_core_get_regions', + 'sportmonks_core_get_entity_filters', + 'sportmonks_core_get_my_usage', 'sportmonks_core_get_region', - 'sportmonks_core_get_cities', - 'sportmonks_core_get_city', - 'sportmonks_core_search_cities', - 'sportmonks_core_get_types', - 'sportmonks_core_get_type', + 'sportmonks_core_get_regions', 'sportmonks_core_get_timezones', + 'sportmonks_core_get_type', + 'sportmonks_core_get_type_by_entity', + 'sportmonks_core_get_types', + 'sportmonks_core_search_cities', + 'sportmonks_core_search_countries', + 'sportmonks_core_search_regions', ], config: { tool: (params) => `sportmonks_${params.operation}`, @@ -595,25 +2164,40 @@ export const SportmonksBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, apiKey: { type: 'string', description: 'Sportmonks API token' }, - date: { type: 'string', description: 'Date in YYYY-MM-DD format' }, - startDate: { type: 'string', description: 'Start date in YYYY-MM-DD format' }, - endDate: { type: 'string', description: 'End date in YYYY-MM-DD format' }, - fixtureId: { type: 'string', description: 'Fixture (session) ID' }, - team1: { type: 'string', description: 'First team ID for head-to-head' }, - team2: { type: 'string', description: 'Second team ID for head-to-head' }, - leagueId: { type: 'string', description: 'League ID' }, - teamId: { type: 'string', description: 'Team ID' }, - driverId: { type: 'string', description: 'Driver ID' }, - playerId: { type: 'string', description: 'Player ID' }, - seasonId: { type: 'string', description: 'Season ID' }, bookmakerId: { type: 'string', description: 'Bookmaker ID' }, - marketId: { type: 'string', description: 'Market ID' }, + cityId: { type: 'string', description: 'City ID' }, + coachId: { type: 'string', description: 'Coach ID' }, continentId: { type: 'string', description: 'Continent ID' }, countryId: { type: 'string', description: 'Country ID' }, + date: { type: 'string', description: 'Date' }, + driverId: { type: 'string', description: 'Driver ID' }, + endDate: { type: 'string', description: 'End Date' }, + fixtureId: { type: 'string', description: 'Fixture ID' }, + fixtureIds: { type: 'string', description: 'Fixture IDs' }, + fromTimestamp: { type: 'string', description: 'From (UNIX timestamp)' }, + ids: { type: 'string', description: 'IDs' }, + lapNumber: { type: 'string', description: 'Lap Number' }, + leagueId: { type: 'string', description: 'League ID' }, + marketId: { type: 'string', description: 'Market ID' }, + playerId: { type: 'string', description: 'Player ID' }, + query: { type: 'string', description: 'Search Query' }, + refereeId: { type: 'string', description: 'Referee ID' }, regionId: { type: 'string', description: 'Region ID' }, - cityId: { type: 'string', description: 'City ID' }, + roundId: { type: 'string', description: 'Round ID' }, + rumourId: { type: 'string', description: 'Rumour ID' }, + seasonId: { type: 'string', description: 'Season ID' }, + stageId: { type: 'string', description: 'Stage ID' }, + startDate: { type: 'string', description: 'Start Date' }, + stateId: { type: 'string', description: 'State ID' }, + stintNumber: { type: 'string', description: 'Stint Number' }, + team1: { type: 'string', description: 'Team 1 ID' }, + team2: { type: 'string', description: 'Team 2 ID' }, + teamId: { type: 'string', description: 'Team ID' }, + toTimestamp: { type: 'string', description: 'To (UNIX timestamp)' }, + transferId: { type: 'string', description: 'Transfer ID' }, + tvStationId: { type: 'string', description: 'TV Station ID' }, typeId: { type: 'string', description: 'Type ID' }, - query: { type: 'string', description: 'Search query' }, + venueId: { type: 'string', description: 'Venue ID' }, include: { type: 'string', description: 'Semicolon-separated relations to include' }, filters: { type: 'string', description: 'Filters to apply' }, per_page: { type: 'string', description: 'Results per page (max 50)' }, @@ -621,59 +2205,80 @@ export const SportmonksBlock: BlockConfig = { order: { type: 'string', description: 'Order direction (asc or desc)' }, }, outputs: { - // Football + Motorsport fixtures - fixtures: { - type: 'json', - description: - 'Array of fixtures/sessions [{id, name, starting_at, league_id, season_id, state_id}] — football and motorsport', - }, - fixture: { type: 'json', description: 'Single fixture/session object' }, - // Football - leagues: { type: 'json', description: 'Array of football leagues' }, - league: { type: 'json', description: 'Single football league object' }, - teams: { type: 'json', description: 'Array of teams (football or motorsport)' }, - team: { type: 'json', description: 'Single team object (football or motorsport)' }, - squad: { type: 'json', description: 'Array of football squad entries' }, - players: { type: 'json', description: 'Array of football players' }, - player: { type: 'json', description: 'Single football player object' }, - standings: { - type: 'json', - description: 'Array of standings (football league or motorsport championship)', - }, - topscorers: { type: 'json', description: 'Array of football topscorers' }, - // Motorsport - drivers: { type: 'json', description: 'Array of motorsport drivers' }, - driver: { type: 'json', description: 'Single motorsport driver object' }, - laps: { type: 'json', description: 'Array of motorsport laps' }, - pitstops: { type: 'json', description: 'Array of motorsport pitstops' }, - // Odds - odds: { - type: 'json', - description: - 'Array of odds [{id, fixture_id, market_id, bookmaker_id, label, value, probability}]', - }, - bookmakers: { type: 'json', description: 'Array of bookmakers [{id, name, logo}]' }, bookmaker: { type: 'json', description: 'Single bookmaker object' }, - markets: { type: 'json', description: 'Array of betting markets [{id, name}]' }, - market: { type: 'json', description: 'Single betting market object' }, - // Core reference - continents: { type: 'json', description: 'Array of continents [{id, name, code}]' }, + bookmakerEvents: { type: 'json', description: 'Array of bookmakerEvents' }, + bookmakers: { type: 'json', description: 'Array of bookmakers' }, + brackets: { type: 'json', description: 'brackets (JSON)' }, + cities: { type: 'json', description: 'Array of cities' }, + city: { type: 'json', description: 'Single city object' }, + coach: { type: 'json', description: 'Single coach object' }, + coaches: { type: 'json', description: 'Array of coaches' }, + commentaries: { type: 'json', description: 'Array of commentaries' }, continent: { type: 'json', description: 'Single continent object' }, - countries: { - type: 'json', - description: 'Array of countries [{id, name, iso2, iso3, image_path}]', - }, + continents: { type: 'json', description: 'Array of continents' }, + corrections: { type: 'json', description: 'Array of corrections' }, + countries: { type: 'json', description: 'Array of countries' }, country: { type: 'json', description: 'Single country object' }, - regions: { type: 'json', description: 'Array of regions [{id, country_id, name}]' }, + driver: { type: 'json', description: 'Single driver object' }, + drivers: { type: 'json', description: 'Array of drivers' }, + entityFilters: { type: 'json', description: 'entityFilters (JSON)' }, + expected: { type: 'json', description: 'Array of expected' }, + expectedLineups: { type: 'json', description: 'Array of expectedLineups' }, + fixture: { type: 'json', description: 'Single fixture object' }, + fixtures: { type: 'json', description: 'Array of fixtures' }, + historicalOdds: { type: 'json', description: 'Array of historicalOdds' }, + laps: { type: 'json', description: 'Array of laps' }, + league: { type: 'json', description: 'Single league object' }, + leagues: { type: 'json', description: 'Array of leagues' }, + market: { type: 'json', description: 'Single market object' }, + markets: { type: 'json', description: 'Array of markets' }, + matchFacts: { type: 'json', description: 'Array of matchFacts' }, + news: { type: 'json', description: 'Array of news' }, + odds: { type: 'json', description: 'Array of odds' }, + pitstops: { type: 'json', description: 'Array of pitstops' }, + player: { type: 'json', description: 'Single player object' }, + players: { type: 'json', description: 'Array of players' }, + predictability: { type: 'json', description: 'Array of predictability' }, + predictions: { type: 'json', description: 'Array of predictions' }, + premiumOdds: { type: 'json', description: 'Array of premiumOdds' }, + referee: { type: 'json', description: 'Single referee object' }, + referees: { type: 'json', description: 'Array of referees' }, region: { type: 'json', description: 'Single region object' }, - cities: { type: 'json', description: 'Array of cities [{id, country_id, name}]' }, - city: { type: 'json', description: 'Single city object' }, - types: { - type: 'json', - description: 'Array of types [{id, name, code, developer_name, group}]', - }, + regions: { type: 'json', description: 'Array of regions' }, + results: { type: 'json', description: 'Array of results' }, + rivals: { type: 'json', description: 'Array of rivals' }, + round: { type: 'json', description: 'Single round object' }, + rounds: { type: 'json', description: 'Array of rounds' }, + schedules: { type: 'json', description: 'Array of schedules' }, + season: { type: 'json', description: 'Single season object' }, + seasons: { type: 'json', description: 'Array of seasons' }, + squad: { type: 'json', description: 'Array of squad' }, + stage: { type: 'json', description: 'Single stage object' }, + stages: { type: 'json', description: 'Array of stages' }, + standings: { type: 'json', description: 'Array of standings' }, + state: { type: 'json', description: 'Single state object' }, + states: { type: 'json', description: 'Array of states' }, + statistics: { type: 'json', description: 'Array of statistics' }, + stints: { type: 'json', description: 'Array of stints' }, + team: { type: 'json', description: 'Single team object' }, + teamRankings: { type: 'json', description: 'Array of teamRankings' }, + teams: { type: 'json', description: 'Array of teams' }, + timezones: { type: 'json', description: 'Array of timezones' }, + topscorers: { type: 'json', description: 'Array of topscorers' }, + totw: { type: 'json', description: 'Array of totw' }, + transfer: { type: 'json', description: 'Single transfer object' }, + transferRumour: { type: 'json', description: 'Single transferRumour object' }, + transferRumours: { type: 'json', description: 'Array of transferRumours' }, + transfers: { type: 'json', description: 'Array of transfers' }, + tvStation: { type: 'json', description: 'Single tvStation object' }, + tvStations: { type: 'json', description: 'Array of tvStations' }, type: { type: 'json', description: 'Single type object' }, - timezones: { type: 'json', description: 'Array of IANA time zone name strings' }, + types: { type: 'json', description: 'Array of types' }, + typesByEntity: { type: 'json', description: 'typesByEntity (JSON)' }, + usage: { type: 'json', description: 'Array of usage' }, + valueBets: { type: 'json', description: 'Array of valueBets' }, + venue: { type: 'json', description: 'Single venue object' }, + venues: { type: 'json', description: 'Array of venues' }, pagination: { type: 'json', description: 'Pagination metadata {count, per_page, current_page, next_page, has_more}', diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index ad85368dcfa..cf2683bc099 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -14936,205 +14936,897 @@ "slug": "sportmonks", "name": "Sportmonks", "description": "Access Sportmonks football, motorsport, odds, and reference data", - "longDescription": "Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, teams, squads, players, standings, and topscorers. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones.", + "longDescription": "Integrate the Sportmonks sports data APIs into the workflow from a single block. Football: fixtures, livescores, leagues, seasons, stages, rounds, teams, squads, players, coaches, referees, venues, standings, topscorers, transfers, schedules, commentaries, TV stations, rivals, expected goals (xG), and predictions. Motorsport: sessions, drivers, teams, championship standings, laps, and pitstops. Odds: pre-match and in-play odds, bookmakers, and markets. Core: continents, countries, regions, cities, types, and time zones.", "bgColor": "#171534", "iconName": "SportmonksIcon", "docsUrl": "https://docs.sim.ai/integrations/sportmonks", "operations": [ { - "name": "Get Live Football Scores", - "description": "Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks" + "name": "Football: Get Expected xG by Player", + "description": "Retrieve lineup-level expected goals (xG) values per player from Sportmonks" }, { - "name": "Get Inplay Football Scores", - "description": "Retrieve all fixtures that are currently being played (in-play) from Sportmonks" + "name": "Football: Get Expected xG by Team", + "description": "Retrieve fixture-level expected goals (xG) values per team from Sportmonks" + }, + { + "name": "Football: Get All Commentaries", + "description": "Retrieve all textual commentaries available within your Sportmonks subscription" + }, + { + "name": "Football: Get All Fixtures", + "description": "Retrieve all football fixtures available within your Sportmonks subscription" + }, + { + "name": "Football: Get All Players", + "description": "Retrieve all football players available within your Sportmonks subscription" + }, + { + "name": "Football: Get All Rivals", + "description": "Retrieve all teams with their rivals information from Sportmonks" + }, + { + "name": "Football: Get All Teams", + "description": "Retrieve all football teams available within your Sportmonks subscription" + }, + { + "name": "Football: Get All Transfer Rumours", + "description": "Retrieve all transfer rumours available within your Sportmonks subscription" + }, + { + "name": "Football: Get All Transfers", + "description": "Retrieve all transfers available within your Sportmonks subscription" + }, + { + "name": "Football: Get Brackets by Season", + "description": "Retrieve the knockout-stage tournament bracket (stages and progression edges) for a season ID" + }, + { + "name": "Football: Get Coach by ID", + "description": "Retrieve a single football coach by their ID from Sportmonks" + }, + { + "name": "Football: Get Coaches", + "description": "Retrieve all football coaches available within your Sportmonks subscription" + }, + { + "name": "Football: Get Coaches by Country", + "description": "Retrieve all coaches for a country ID from Sportmonks" + }, + { + "name": "Football: Get Commentaries by Fixture", + "description": "Retrieve textual commentary for a fixture by fixture ID from Sportmonks" }, { - "name": "Get Football Fixtures by Date", + "name": "Football: Get Current Leagues by Team", + "description": "Retrieve all current leagues for a team ID from Sportmonks" + }, + { + "name": "Football: Get Expected Lineups by Player", + "description": "Retrieve the premium expected lineups for a player ID from Sportmonks" + }, + { + "name": "Football: Get Expected Lineups by Team", + "description": "Retrieve the premium expected lineups for a team ID from Sportmonks" + }, + { + "name": "Football: Get Extended Team Squad", + "description": "Retrieve all squad entries for a team (based on current seasons) by team ID" + }, + { + "name": "Football: Get Fixture by ID", + "description": "Retrieve a single football fixture by its ID from Sportmonks" + }, + { + "name": "Football: Get Fixtures by Date", "description": "Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks" }, { - "name": "Get Football Fixtures by Date Range", + "name": "Football: Get Fixtures by Date Range", "description": "Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max range is 100 days." }, { - "name": "Get Football Fixture by ID", - "description": "Retrieve a single football fixture by its ID from Sportmonks" + "name": "Football: Get Fixtures by Date Range for Team", + "description": "Retrieve fixtures for a team within a date range (YYYY-MM-DD) from Sportmonks" }, { - "name": "Get Football Head to Head", + "name": "Football: Get Fixtures by Multiple IDs", + "description": "Retrieve multiple football fixtures by a comma-separated list of IDs (max 50)" + }, + { + "name": "Football: Get Grouped Standings by Round", + "description": "Retrieve the standing table for a round ID grouped by group where applicable from Sportmonks" + }, + { + "name": "Football: Get Head to Head", "description": "Retrieve the head-to-head fixtures between two teams from Sportmonks" }, { - "name": "Get Football Leagues", - "description": "Retrieve all football leagues available within your Sportmonks subscription" + "name": "Football: Get Inplay Livescores", + "description": "Retrieve all fixtures that are currently being played (in-play) from Sportmonks" + }, + { + "name": "Football: Get Last Updated Coaches", + "description": "Retrieve all coaches that have received updates in the past two hours" + }, + { + "name": "Football: Get Latest Updated Fixtures", + "description": "Retrieve all fixtures that have received updates within the last 10 seconds" + }, + { + "name": "Football: Get Latest Updated Livescores", + "description": "Retrieve all livescores that have received updates within the last 10 seconds" + }, + { + "name": "Football: Get Last Updated Players", + "description": "Retrieve all players that have received updates in the past two hours" + }, + { + "name": "Football: Get Latest Team of the Week", + "description": "Retrieve the latest Team of the Week (TOTW) for a league ID from Sportmonks" }, { - "name": "Get Football League by ID", + "name": "Football: Get Latest Transfers", + "description": "Retrieve the latest transfers available within your Sportmonks subscription" + }, + { + "name": "Football: Get League by ID", "description": "Retrieve a single football league by its ID from Sportmonks" }, { - "name": "Search Football Teams", - "description": "Search for football teams by name from Sportmonks" + "name": "Football: Get Leagues", + "description": "Retrieve all football leagues available within your Sportmonks subscription" }, { - "name": "Get Football Team by ID", - "description": "Retrieve a single football team by its ID from Sportmonks" + "name": "Football: Get Leagues by Country", + "description": "Retrieve all leagues for a country ID from Sportmonks" }, { - "name": "Get Football Team Squad", - "description": "Retrieve the current domestic squad for a team by team ID from Sportmonks" + "name": "Football: Get Leagues by Date", + "description": "Retrieve all leagues with fixtures on a given date (YYYY-MM-DD) from Sportmonks" }, { - "name": "Search Football Players", - "description": "Search for football players by name from Sportmonks" + "name": "Football: Get Leagues by Team", + "description": "Retrieve all current and historical leagues for a team ID from Sportmonks" + }, + { + "name": "Football: Get Live Leagues", + "description": "Retrieve all leagues that have fixtures currently being played from Sportmonks" + }, + { + "name": "Football: Get Live Probabilities", + "description": "Retrieve all live (in-play) prediction probabilities from Sportmonks" + }, + { + "name": "Football: Get Live Probabilities by Fixture", + "description": "Retrieve all live (in-play) prediction probabilities for a fixture ID from Sportmonks" }, { - "name": "Get Football Player by ID", + "name": "Football: Get Live Standings by League", + "description": "Retrieve the live standing table for a league ID from Sportmonks" + }, + { + "name": "Football: Get Livescores", + "description": "Retrieve fixtures starting within 15 minutes and currently in progress from Sportmonks" + }, + { + "name": "Football: Get All Match Facts", + "description": "Retrieve all available match facts within your Sportmonks subscription (beta)" + }, + { + "name": "Football: Get Match Facts by Date Range", + "description": "Retrieve match facts within a date range (YYYY-MM-DD) from Sportmonks (beta)" + }, + { + "name": "Football: Get Match Facts by Fixture", + "description": "Retrieve match facts for a fixture ID from Sportmonks (beta)" + }, + { + "name": "Football: Get Match Facts by League", + "description": "Retrieve match facts for a league ID from Sportmonks (beta)" + }, + { + "name": "Football: Get Past Fixtures by TV Station", + "description": "Retrieve all past fixtures that were available for a TV station ID from Sportmonks" + }, + { + "name": "Football: Get Player by ID", "description": "Retrieve a single football player by their ID from Sportmonks" }, { - "name": "Get Football Standings by Season", + "name": "Football: Get Players by Country", + "description": "Retrieve all players for a country ID from Sportmonks" + }, + { + "name": "Football: Get Post-Match News", + "description": "Retrieve all post-match news articles available within your Sportmonks subscription" + }, + { + "name": "Football: Get Post-Match News by Season", + "description": "Retrieve all post-match news articles for a season ID from Sportmonks" + }, + { + "name": "Football: Get Predictability by League", + "description": "Retrieve the predictions model performance for a league ID from Sportmonks" + }, + { + "name": "Football: Get Pre-Match News", + "description": "Retrieve all pre-match news articles available within your Sportmonks subscription" + }, + { + "name": "Football: Get Pre-Match News by Season", + "description": "Retrieve all pre-match news articles for a season ID from Sportmonks" + }, + { + "name": "Football: Get Pre-Match News for Upcoming Fixtures", + "description": "Retrieve all pre-match news articles for upcoming fixtures from Sportmonks" + }, + { + "name": "Football: Get Probabilities", + "description": "Retrieve all prediction probabilities available within your Sportmonks subscription" + }, + { + "name": "Football: Get Predictions by Fixture", + "description": "Retrieve prediction probabilities for a fixture by fixture ID from Sportmonks" + }, + { + "name": "Football: Get Referee by ID", + "description": "Retrieve a single football referee by their ID from Sportmonks" + }, + { + "name": "Football: Get Referees", + "description": "Retrieve all football referees available within your Sportmonks subscription" + }, + { + "name": "Football: Get Referees by Country", + "description": "Retrieve all referees for a country ID from Sportmonks" + }, + { + "name": "Football: Get Referees by Season", + "description": "Retrieve all referees for a season ID from Sportmonks" + }, + { + "name": "Football: Get Rivals by Team", + "description": "Retrieve rival teams for a team by team ID from Sportmonks" + }, + { + "name": "Football: Get Round by ID", + "description": "Retrieve a single football round by its ID from Sportmonks" + }, + { + "name": "Football: Get Round Statistics", + "description": "Retrieve all available statistics for a round ID from Sportmonks" + }, + { + "name": "Football: Get Rounds", + "description": "Retrieve all football rounds available within your Sportmonks subscription" + }, + { + "name": "Football: Get Rounds by Season", + "description": "Retrieve all rounds for a season ID from Sportmonks" + }, + { + "name": "Football: Get Schedules by Season", + "description": "Retrieve the full schedule (stages, rounds and fixtures) for a season by season ID" + }, + { + "name": "Football: Get Schedules by Season and Team", + "description": "Retrieve the full season schedule for a specific team by season ID and team ID" + }, + { + "name": "Football: Get Schedules by Team", + "description": "Retrieve the full schedule (stages, rounds and fixtures) for a team by team ID" + }, + { + "name": "Football: Get Season by ID", + "description": "Retrieve a single football season by its ID from Sportmonks" + }, + { + "name": "Football: Get Seasons", + "description": "Retrieve all football seasons available within your Sportmonks subscription" + }, + { + "name": "Football: Get Seasons by Team", + "description": "Retrieve all seasons for a team ID from Sportmonks" + }, + { + "name": "Football: Get Stage by ID", + "description": "Retrieve a single football stage by its ID from Sportmonks" + }, + { + "name": "Football: Get Stage Statistics", + "description": "Retrieve all available statistics for a stage ID from Sportmonks" + }, + { + "name": "Football: Get Stages", + "description": "Retrieve all football stages available within your Sportmonks subscription" + }, + { + "name": "Football: Get Stages by Season", + "description": "Retrieve all stages for a season ID from Sportmonks" + }, + { + "name": "Football: Get Standing Corrections by Season", + "description": "Retrieve point corrections (awarded or deducted) for a season ID from Sportmonks" + }, + { + "name": "Football: Get All Standings", + "description": "Retrieve all standings available within your Sportmonks subscription" + }, + { + "name": "Football: Get Standings by Round", + "description": "Retrieve the full standing table for a round ID from Sportmonks" + }, + { + "name": "Football: Get Standings by Season", "description": "Retrieve the full league standings table for a season by season ID from Sportmonks" }, { - "name": "Get Football Topscorers by Season", + "name": "Football: Get State by ID", + "description": "Retrieve a single fixture state by its ID from Sportmonks" + }, + { + "name": "Football: Get States", + "description": "Retrieve all fixture states (e.g. Not Started, 1st Half, Full Time) from Sportmonks" + }, + { + "name": "Football: Get Team by ID", + "description": "Retrieve a single football team by its ID from Sportmonks" + }, + { + "name": "Football: Get All Team Rankings", + "description": "Retrieve all team rankings available within your Sportmonks subscription (beta)" + }, + { + "name": "Football: Get Team Rankings by Date", + "description": "Retrieve team rankings for a given date (YYYY-MM-DD) from Sportmonks (beta)" + }, + { + "name": "Football: Get Team Rankings by Team", + "description": "Retrieve team rankings for a team ID from Sportmonks (beta)" + }, + { + "name": "Football: Get Team Squad", + "description": "Retrieve the current domestic squad for a team by team ID from Sportmonks" + }, + { + "name": "Football: Get Team Squad by Season", + "description": "Retrieve the (historical) squad for a team in a specific season from Sportmonks" + }, + { + "name": "Football: Get Teams by Country", + "description": "Retrieve all teams for a country ID from Sportmonks" + }, + { + "name": "Football: Get Teams by Season", + "description": "Retrieve all teams for a season ID from Sportmonks" + }, + { + "name": "Football: Get Topscorers by Season", "description": "Retrieve the topscorers (goals, assists, cards) for a season by season ID from Sportmonks" }, { - "name": "Get Live Motorsport Scores", - "description": "Retrieve all live motorsport fixtures (sessions) from Sportmonks" + "name": "Football: Get Topscorers by Stage", + "description": "Retrieve topscorers for a stage by stage ID from Sportmonks" }, { - "name": "Get Motorsport Fixtures by Date", - "description": "Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks" + "name": "Football: Get All Team of the Week", + "description": "Retrieve all available Team of the Week (TOTW) entries from Sportmonks" }, { - "name": "Get Motorsport Fixture by ID", - "description": "Retrieve a single motorsport fixture (session) by its ID from Sportmonks" + "name": "Football: Get Team of the Week by Round", + "description": "Retrieve the Team of the Week (TOTW) for a round ID from Sportmonks" }, { - "name": "Get Motorsport Drivers", - "description": "Retrieve all motorsport drivers from Sportmonks" + "name": "Football: Get Transfer by ID", + "description": "Retrieve a single transfer by its ID from Sportmonks" }, { - "name": "Get Motorsport Driver by ID", - "description": "Retrieve a single motorsport driver by their ID from Sportmonks" + "name": "Football: Get Transfer Rumour by ID", + "description": "Retrieve a single transfer rumour by its ID from Sportmonks" }, { - "name": "Search Motorsport Drivers", - "description": "Search for motorsport drivers by name from Sportmonks" + "name": "Football: Get Transfer Rumours Between Dates", + "description": "Retrieve transfer rumours within a date range (YYYY-MM-DD) from Sportmonks" }, { - "name": "Get Motorsport Teams", - "description": "Retrieve all motorsport teams (constructors) from Sportmonks" + "name": "Football: Get Transfer Rumours by Player", + "description": "Retrieve transfer rumours for a player ID from Sportmonks" }, { - "name": "Get Motorsport Team by ID", - "description": "Retrieve a single motorsport team (constructor) by its ID from Sportmonks" + "name": "Football: Get Transfer Rumours by Team", + "description": "Retrieve transfer rumours for a team ID from Sportmonks" + }, + { + "name": "Football: Get Transfers Between Dates", + "description": "Retrieve transfers within a date range (YYYY-MM-DD) from Sportmonks" + }, + { + "name": "Football: Get Transfers by Player", + "description": "Retrieve transfers for a player by player ID from Sportmonks" + }, + { + "name": "Football: Get Transfers by Team", + "description": "Retrieve transfers for a team by team ID from Sportmonks" + }, + { + "name": "Football: Get TV Station by ID", + "description": "Retrieve a single TV station by its ID from Sportmonks" + }, + { + "name": "Football: Get TV Stations", + "description": "Retrieve all TV stations available within your Sportmonks subscription" + }, + { + "name": "Football: Get TV Stations by Fixture", + "description": "Retrieve broadcasting TV stations for a fixture by fixture ID from Sportmonks" + }, + { + "name": "Football: Get Upcoming Fixtures by Market", + "description": "Retrieve all upcoming fixtures for a market ID from Sportmonks" + }, + { + "name": "Football: Get Upcoming Fixtures by TV Station", + "description": "Retrieve all upcoming fixtures available for a TV station ID from Sportmonks" + }, + { + "name": "Football: Get Value Bets", + "description": "Retrieve all value bets available within your Sportmonks subscription" + }, + { + "name": "Football: Get Value Bets by Fixture", + "description": "Retrieve value bet predictions for a fixture by fixture ID from Sportmonks" + }, + { + "name": "Football: Get Venue by ID", + "description": "Retrieve a single football venue by its ID from Sportmonks" + }, + { + "name": "Football: Get Venues", + "description": "Retrieve all football venues available within your Sportmonks subscription" + }, + { + "name": "Football: Get Venues by Season", + "description": "Retrieve all venues for a season ID from Sportmonks" + }, + { + "name": "Football: Search Coaches", + "description": "Search for football coaches by name from Sportmonks" + }, + { + "name": "Football: Search Fixtures", + "description": "Search for football fixtures by name (participants) from Sportmonks" + }, + { + "name": "Football: Search Leagues", + "description": "Search for football leagues by name from Sportmonks" + }, + { + "name": "Football: Search Players", + "description": "Search for football players by name from Sportmonks" + }, + { + "name": "Football: Search Referees", + "description": "Search for football referees by name from Sportmonks" + }, + { + "name": "Football: Search Rounds", + "description": "Search for football rounds by name from Sportmonks" + }, + { + "name": "Football: Search Seasons", + "description": "Search for football seasons by name from Sportmonks" + }, + { + "name": "Football: Search Stages", + "description": "Search for football stages by name from Sportmonks" + }, + { + "name": "Football: Search Teams", + "description": "Search for football teams by name from Sportmonks" + }, + { + "name": "Football: Search Venues", + "description": "Search for football venues by name from Sportmonks" + }, + { + "name": "Motorsport: Get All Fixtures", + "description": "Retrieve all motorsport fixtures (sessions) from Sportmonks" + }, + { + "name": "Motorsport: Get Current Leagues by Team", + "description": "Retrieve the current motorsport leagues for a team by team ID from Sportmonks" + }, + { + "name": "Motorsport: Get Driver by ID", + "description": "Retrieve a single motorsport driver by their ID from Sportmonks" }, { - "name": "Get Motorsport Driver Standings by Season", + "name": "Motorsport: Get All Driver Standings", + "description": "Retrieve all driver championship standings from Sportmonks" + }, + { + "name": "Motorsport: Get Driver Standings by Season", "description": "Retrieve the drivers championship standings for a season by season ID from Sportmonks" }, { - "name": "Get Motorsport Team Standings by Season", - "description": "Retrieve the constructors championship standings for a season by season ID from Sportmonks" + "name": "Motorsport: Get Drivers", + "description": "Retrieve all motorsport drivers from Sportmonks" }, { - "name": "Get Motorsport Laps by Fixture", + "name": "Motorsport: Get Drivers by Country", + "description": "Retrieve all motorsport drivers for a country by country ID from Sportmonks" + }, + { + "name": "Motorsport: Get Drivers by Season", + "description": "Retrieve all motorsport drivers for a season by season ID from Sportmonks" + }, + { + "name": "Motorsport: Get Fixture by ID", + "description": "Retrieve a single motorsport fixture (session) by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get Fixtures by Date", + "description": "Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Sportmonks" + }, + { + "name": "Motorsport: Get Fixtures by Date Range", + "description": "Retrieve motorsport fixtures (sessions) between two dates (YYYY-MM-DD, max 100 days) from Sportmonks" + }, + { + "name": "Motorsport: Get Fixtures by IDs", + "description": "Retrieve multiple motorsport fixtures (sessions) by their IDs (max 50) from Sportmonks" + }, + { + "name": "Motorsport: Get Laps by Fixture", "description": "Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmonks" }, { - "name": "Get Motorsport Pitstops by Fixture", + "name": "Motorsport: Get Laps by Fixture and Driver", + "description": "Retrieve all laps for a motorsport fixture and driver from Sportmonks" + }, + { + "name": "Motorsport: Get Laps by Fixture and Lap Number", + "description": "Retrieve all laps for a motorsport fixture and lap number from Sportmonks" + }, + { + "name": "Motorsport: Get Latest Laps by Fixture", + "description": "Retrieve the latest laps for a motorsport fixture (session) by fixture ID from Sportmonks" + }, + { + "name": "Motorsport: Get Latest Pitstops by Fixture", + "description": "Retrieve the latest pitstops for a motorsport fixture (session) by fixture ID from Sportmonks" + }, + { + "name": "Motorsport: Get Latest Stints by Fixture", + "description": "Retrieve the latest tyre stints for a motorsport fixture (session) by fixture ID from Sportmonks" + }, + { + "name": "Motorsport: Get Latest Updated Drivers", + "description": "Retrieve the most recently updated motorsport drivers from Sportmonks" + }, + { + "name": "Motorsport: Get Latest Updated Fixtures", + "description": "Retrieve the most recently updated motorsport fixtures (sessions) from Sportmonks" + }, + { + "name": "Motorsport: Get League by ID", + "description": "Retrieve a single motorsport league by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get All Leagues", + "description": "Retrieve all motorsport leagues from Sportmonks" + }, + { + "name": "Motorsport: Get Leagues by Country", + "description": "Retrieve all motorsport leagues for a country by country ID from Sportmonks" + }, + { + "name": "Motorsport: Get Leagues by Fixture Date", + "description": "Retrieve all motorsport leagues with fixtures on a specific date (YYYY-MM-DD) from Sportmonks" + }, + { + "name": "Motorsport: Get Leagues by Live", + "description": "Retrieve all motorsport leagues that currently have live fixtures from Sportmonks" + }, + { + "name": "Motorsport: Get Leagues by Team", + "description": "Retrieve all current and historical motorsport leagues for a team by team ID from Sportmonks" + }, + { + "name": "Motorsport: Get Livescores", + "description": "Retrieve all live motorsport fixtures (sessions) from Sportmonks" + }, + { + "name": "Motorsport: Get Pitstops by Fixture", "description": "Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Sportmonks" }, { - "name": "Get Pre-match Odds by Fixture", - "description": "Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API" + "name": "Motorsport: Get Pitstops by Fixture and Driver", + "description": "Retrieve all pitstops for a motorsport fixture and driver from Sportmonks" }, { - "name": "Get In-play Odds by Fixture", - "description": "Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API" + "name": "Motorsport: Get Pitstops by Fixture and Lap Number", + "description": "Retrieve all pitstops for a motorsport fixture and lap number from Sportmonks" }, { - "name": "Get Bookmakers", - "description": "Retrieve all bookmakers from the Sportmonks Odds API" + "name": "Motorsport: Get Race Results by Season and Driver", + "description": "Retrieve race results (stages with fixtures, lineups and lineup details) for a season and driver from Sportmonks" }, { - "name": "Get Bookmaker by ID", + "name": "Motorsport: Get Race Results by Season and Team", + "description": "Retrieve race results (stages with fixtures, lineups and lineup details) for a season and team from Sportmonks" + }, + { + "name": "Motorsport: Get Schedules by Season", + "description": "Retrieve the full schedule (stages with nested fixtures and venues) for a season by season ID from Sportmonks" + }, + { + "name": "Motorsport: Get Season by ID", + "description": "Retrieve a single motorsport season by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get All Seasons", + "description": "Retrieve all motorsport seasons from Sportmonks" + }, + { + "name": "Motorsport: Get Stage by ID", + "description": "Retrieve a single motorsport stage (race weekend) by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get All Stages", + "description": "Retrieve all motorsport stages (race weekends) from Sportmonks" + }, + { + "name": "Motorsport: Get Stages by Season", + "description": "Retrieve all motorsport stages (race weekends) for a season by season ID from Sportmonks" + }, + { + "name": "Motorsport: Get State by ID", + "description": "Retrieve a single motorsport fixture state by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get All States", + "description": "Retrieve all possible motorsport fixture states from Sportmonks" + }, + { + "name": "Motorsport: Get Stints by Fixture", + "description": "Retrieve all tyre stints for a motorsport fixture (session) by fixture ID from Sportmonks" + }, + { + "name": "Motorsport: Get Stints by Fixture and Driver", + "description": "Retrieve all tyre stints for a motorsport fixture and driver from Sportmonks" + }, + { + "name": "Motorsport: Get Stints by Fixture and Stint Number", + "description": "Retrieve all tyre stints for a motorsport fixture and stint number from Sportmonks" + }, + { + "name": "Motorsport: Get Team by ID", + "description": "Retrieve a single motorsport team (constructor) by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get All Team Standings", + "description": "Retrieve all team (constructor) championship standings from Sportmonks" + }, + { + "name": "Motorsport: Get Team Standings by Season", + "description": "Retrieve the constructors championship standings for a season by season ID from Sportmonks" + }, + { + "name": "Motorsport: Get Teams", + "description": "Retrieve all motorsport teams (constructors) from Sportmonks" + }, + { + "name": "Motorsport: Get Teams by Country", + "description": "Retrieve all motorsport teams (constructors) for a country by country ID from Sportmonks" + }, + { + "name": "Motorsport: Get Teams by Season", + "description": "Retrieve all motorsport teams (constructors) for a season by season ID from Sportmonks" + }, + { + "name": "Motorsport: Get Venue by ID", + "description": "Retrieve a single motorsport venue (racing track) by its ID from Sportmonks" + }, + { + "name": "Motorsport: Get Venues", + "description": "Retrieve all motorsport venues (racing tracks) from Sportmonks" + }, + { + "name": "Motorsport: Get Venues by Season", + "description": "Retrieve all motorsport venues (racing tracks) for a season by season ID from Sportmonks" + }, + { + "name": "Motorsport: Search Drivers", + "description": "Search for motorsport drivers by name from Sportmonks" + }, + { + "name": "Motorsport: Search Leagues", + "description": "Search for motorsport leagues by name from Sportmonks" + }, + { + "name": "Motorsport: Search Stages", + "description": "Search for motorsport stages (race weekends) by name from Sportmonks" + }, + { + "name": "Motorsport: Search Teams", + "description": "Search for motorsport teams (constructors) by name from Sportmonks" + }, + { + "name": "Motorsport: Search Venues", + "description": "Search for motorsport venues (racing tracks) by name from Sportmonks" + }, + { + "name": "Odds: Get All Historical Odds", + "description": "Retrieve all available historical (premium) pre-match odd values from the Sportmonks Odds API" + }, + { + "name": "Odds: Get All In-play Odds", + "description": "Retrieve all available live (in-play) odds from the Sportmonks Odds API" + }, + { + "name": "Odds: Get All Pre-match Odds", + "description": "Retrieve all available pre-match odds from the Sportmonks Odds API" + }, + { + "name": "Odds: Get All Premium Odds", + "description": "Retrieve all available premium (historical) pre-match odds from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Bookmaker by ID", "description": "Retrieve a single bookmaker by its ID from the Sportmonks Odds API" }, { - "name": "Search Bookmakers", - "description": "Search for bookmakers by name from the Sportmonks Odds API" + "name": "Odds: Get Bookmaker Event IDs by Fixture", + "description": "Retrieve bookmakers' own event ids mapped to a Sportmonks fixture via the Sportmonks Odds API" }, { - "name": "Get Betting Markets", - "description": "Retrieve all betting markets from the Sportmonks Odds API" + "name": "Odds: Get Bookmakers", + "description": "Retrieve all bookmakers from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Bookmakers by Fixture", + "description": "Retrieve all bookmakers available for a fixture from the Sportmonks Odds API" + }, + { + "name": "Odds: Get In-play Odds by Fixture", + "description": "Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odds API" + }, + { + "name": "Odds: Get In-play Odds by Fixture and Bookmaker", + "description": "Retrieve live (in-play) odds for a fixture from a specific bookmaker via the Sportmonks Odds API" }, { - "name": "Get Betting Market by ID", + "name": "Odds: Get In-play Odds by Fixture and Market", + "description": "Retrieve live (in-play) odds for a fixture on a specific market via the Sportmonks Odds API" + }, + { + "name": "Odds: Get Last Updated In-play Odds", + "description": "Retrieve in-play odds updated in the last 10 seconds from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Last Updated Pre-match Odds", + "description": "Retrieve pre-match odds updated in the last 10 seconds from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Market by ID", "description": "Retrieve a single betting market by its ID from the Sportmonks Odds API" }, { - "name": "Search Betting Markets", + "name": "Odds: Get Markets", + "description": "Retrieve all betting markets from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Pre-match Odds by Fixture", + "description": "Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Pre-match Odds by Fixture and Bookmaker", + "description": "Retrieve pre-match odds for a fixture from a specific bookmaker via the Sportmonks Odds API" + }, + { + "name": "Odds: Get Pre-match Odds by Fixture and Market", + "description": "Retrieve pre-match odds for a fixture on a specific market via the Sportmonks Odds API" + }, + { + "name": "Odds: Get Premium Odds by Fixture", + "description": "Retrieve premium (historical) pre-match odds for a fixture from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Premium Odds by Fixture and Bookmaker", + "description": "Retrieve premium pre-match odds for a fixture from a specific bookmaker via the Sportmonks Odds API" + }, + { + "name": "Odds: Get Premium Odds by Fixture and Market", + "description": "Retrieve premium pre-match odds for a fixture on a specific market via the Sportmonks Odds API" + }, + { + "name": "Odds: Get Updated Historical Odds Between Time Range", + "description": "Retrieve historical (premium) odds updated between two UNIX timestamps (max 5 minutes) from the Sportmonks Odds API" + }, + { + "name": "Odds: Get Updated Premium Odds Between Time Range", + "description": "Retrieve premium odds updated between two UNIX timestamps (max 5 minutes) from the Sportmonks Odds API" + }, + { + "name": "Odds: Search Bookmakers", + "description": "Search for bookmakers by name from the Sportmonks Odds API" + }, + { + "name": "Odds: Search Markets", "description": "Search for betting markets by name from the Sportmonks Odds API" }, { - "name": "Get Continents", - "description": "Retrieve all continents from the Sportmonks Core API" + "name": "Core: Get Cities", + "description": "Retrieve all cities from the Sportmonks Core API" }, { - "name": "Get Continent by ID", + "name": "Core: Get City by ID", + "description": "Retrieve a single city by its ID from the Sportmonks Core API" + }, + { + "name": "Core: Get Continent by ID", "description": "Retrieve a single continent by its ID from the Sportmonks Core API" }, { - "name": "Get Countries", + "name": "Core: Get Continents", + "description": "Retrieve all continents from the Sportmonks Core API" + }, + { + "name": "Core: Get Countries", "description": "Retrieve all countries from the Sportmonks Core API" }, { - "name": "Get Country by ID", + "name": "Core: Get Country by ID", "description": "Retrieve a single country by its ID from the Sportmonks Core API" }, { - "name": "Search Countries", - "description": "Search for countries by name from the Sportmonks Core API" + "name": "Core: Get All Entity Filters", + "description": "Retrieve all available filters grouped per entity from the Sportmonks Core API" }, { - "name": "Get Regions", - "description": "Retrieve all regions from the Sportmonks Core API" + "name": "Core: Get My Usage", + "description": "Retrieve your Sportmonks API usage aggregated per 5 minutes" }, { - "name": "Get Region by ID", + "name": "Core: Get Region by ID", "description": "Retrieve a single region by its ID from the Sportmonks Core API" }, { - "name": "Get Cities", - "description": "Retrieve all cities from the Sportmonks Core API" + "name": "Core: Get Regions", + "description": "Retrieve all regions from the Sportmonks Core API" }, { - "name": "Get City by ID", - "description": "Retrieve a single city by its ID from the Sportmonks Core API" + "name": "Core: Get Timezones", + "description": "Retrieve all supported time zones (IANA names) from the Sportmonks Core API" }, { - "name": "Search Cities", - "description": "Search for cities by name from the Sportmonks Core API" + "name": "Core: Get Type by ID", + "description": "Retrieve a single type by its ID from the Sportmonks Core API" + }, + { + "name": "Core: Get Type by Entity", + "description": "Retrieve the available types grouped per entity from the Sportmonks Core API" }, { - "name": "Get Types", + "name": "Core: Get Types", "description": "Retrieve all types (reference data describing events, statistics, positions, etc.) from the Sportmonks Core API" }, { - "name": "Get Type by ID", - "description": "Retrieve a single type by its ID from the Sportmonks Core API" + "name": "Core: Search Cities", + "description": "Search for cities by name from the Sportmonks Core API" }, { - "name": "Get Timezones", - "description": "Retrieve all supported time zones (IANA names) from the Sportmonks Core API" + "name": "Core: Search Countries", + "description": "Search for countries by name from the Sportmonks Core API" + }, + { + "name": "Core: Search Regions", + "description": "Search for regions by name from the Sportmonks Core API" } ], - "operationCount": 48, + "operationCount": 221, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 43d9eb05cb3..3c40cdf7b5c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3109,52 +3109,225 @@ import { sportmonksCoreGetContinentTool, sportmonksCoreGetCountriesTool, sportmonksCoreGetCountryTool, + sportmonksCoreGetEntityFiltersTool, + sportmonksCoreGetMyUsageTool, sportmonksCoreGetRegionsTool, sportmonksCoreGetRegionTool, sportmonksCoreGetTimezonesTool, + sportmonksCoreGetTypeByEntityTool, sportmonksCoreGetTypesTool, sportmonksCoreGetTypeTool, sportmonksCoreSearchCitiesTool, sportmonksCoreSearchCountriesTool, + sportmonksCoreSearchRegionsTool, } from '@/tools/sportmonks_core' import { + sportmonksExpectedByPlayerTool, + sportmonksExpectedByTeamTool, + sportmonksGetAllCommentariesTool, + sportmonksGetAllFixturesTool, + sportmonksGetAllPlayersTool, + sportmonksGetAllRivalsTool, + sportmonksGetAllTeamsTool, + sportmonksGetAllTransferRumoursTool, + sportmonksGetAllTransfersTool, + sportmonksGetBracketsBySeasonTool, + sportmonksGetCoachesByCountryTool, + sportmonksGetCoachesTool, + sportmonksGetCoachTool, + sportmonksGetCommentariesByFixtureTool, + sportmonksGetCurrentLeaguesByTeamTool, + sportmonksGetExpectedLineupsByPlayerTool, + sportmonksGetExpectedLineupsByTeamTool, + sportmonksGetExtendedTeamSquadTool, + sportmonksGetFixturesByDateRangeForTeamTool, sportmonksGetFixturesByDateRangeTool, sportmonksGetFixturesByDateTool, + sportmonksGetFixturesByIdsTool, sportmonksGetFixtureTool, + sportmonksGetGroupedStandingsByRoundTool, sportmonksGetHeadToHeadTool, sportmonksGetInplayLivescoresTool, + sportmonksGetLatestCoachesTool, + sportmonksGetLatestFixturesTool, + sportmonksGetLatestLivescoresTool, + sportmonksGetLatestPlayersTool, + sportmonksGetLatestTotwTool, + sportmonksGetLatestTransfersTool, + sportmonksGetLeaguesByCountryTool, + sportmonksGetLeaguesByDateTool, + sportmonksGetLeaguesByTeamTool, sportmonksGetLeaguesTool, sportmonksGetLeagueTool, + sportmonksGetLiveLeaguesTool, + sportmonksGetLiveProbabilitiesByFixtureTool, + sportmonksGetLiveProbabilitiesTool, + sportmonksGetLiveStandingsByLeagueTool, sportmonksGetLivescoresTool, + sportmonksGetMatchFactsByDateRangeTool, + sportmonksGetMatchFactsByFixtureTool, + sportmonksGetMatchFactsByLeagueTool, + sportmonksGetMatchFactsTool, + sportmonksGetPastFixturesByTvStationTool, + sportmonksGetPlayersByCountryTool, sportmonksGetPlayerTool, + sportmonksGetPostmatchNewsBySeasonTool, + sportmonksGetPostmatchNewsTool, + sportmonksGetPredictabilityByLeagueTool, + sportmonksGetPrematchNewsBySeasonTool, + sportmonksGetPrematchNewsTool, + sportmonksGetPrematchNewsUpcomingTool, + sportmonksGetProbabilitiesByFixtureTool, + sportmonksGetProbabilitiesTool, + sportmonksGetRefereesByCountryTool, + sportmonksGetRefereesBySeasonTool, + sportmonksGetRefereesTool, + sportmonksGetRefereeTool, + sportmonksGetRivalsByTeamTool, + sportmonksGetRoundStatisticsTool, + sportmonksGetRoundsBySeasonTool, + sportmonksGetRoundsTool, + sportmonksGetRoundTool, + sportmonksGetSchedulesBySeasonAndTeamTool, + sportmonksGetSchedulesBySeasonTool, + sportmonksGetSchedulesByTeamTool, + sportmonksGetSeasonsByTeamTool, + sportmonksGetSeasonsTool, + sportmonksGetSeasonTool, + sportmonksGetStageStatisticsTool, + sportmonksGetStagesBySeasonTool, + sportmonksGetStagesTool, + sportmonksGetStageTool, + sportmonksGetStandingCorrectionsBySeasonTool, + sportmonksGetStandingsByRoundTool, sportmonksGetStandingsBySeasonTool, + sportmonksGetStandingsTool, + sportmonksGetStatesTool, + sportmonksGetStateTool, + sportmonksGetTeamRankingsByDateTool, + sportmonksGetTeamRankingsByTeamTool, + sportmonksGetTeamRankingsTool, + sportmonksGetTeamSquadBySeasonTool, sportmonksGetTeamSquadTool, + sportmonksGetTeamsByCountryTool, + sportmonksGetTeamsBySeasonTool, sportmonksGetTeamTool, sportmonksGetTopscorersBySeasonTool, + sportmonksGetTopscorersByStageTool, + sportmonksGetTotwByRoundTool, + sportmonksGetTotwTool, + sportmonksGetTransferRumoursBetweenDatesTool, + sportmonksGetTransferRumoursByPlayerTool, + sportmonksGetTransferRumoursByTeamTool, + sportmonksGetTransferRumourTool, + sportmonksGetTransfersBetweenDatesTool, + sportmonksGetTransfersByPlayerTool, + sportmonksGetTransfersByTeamTool, + sportmonksGetTransferTool, + sportmonksGetTvStationsByFixtureTool, + sportmonksGetTvStationsTool, + sportmonksGetTvStationTool, + sportmonksGetUpcomingFixturesByMarketTool, + sportmonksGetUpcomingFixturesByTvStationTool, + sportmonksGetValueBetsByFixtureTool, + sportmonksGetValueBetsTool, + sportmonksGetVenuesBySeasonTool, + sportmonksGetVenuesTool, + sportmonksGetVenueTool, + sportmonksSearchCoachesTool, + sportmonksSearchFixturesTool, + sportmonksSearchLeaguesTool, sportmonksSearchPlayersTool, + sportmonksSearchRefereesTool, + sportmonksSearchRoundsTool, + sportmonksSearchSeasonsTool, + sportmonksSearchStagesTool, sportmonksSearchTeamsTool, + sportmonksSearchVenuesTool, } from '@/tools/sportmonks_football' import { + sportmonksMotorsportGetAllFixturesTool, + sportmonksMotorsportGetCurrentLeaguesByTeamTool, sportmonksMotorsportGetDriverStandingsBySeasonTool, + sportmonksMotorsportGetDriverStandingsTool, + sportmonksMotorsportGetDriversByCountryTool, + sportmonksMotorsportGetDriversBySeasonTool, sportmonksMotorsportGetDriversTool, sportmonksMotorsportGetDriverTool, + sportmonksMotorsportGetFixturesByDateRangeTool, sportmonksMotorsportGetFixturesByDateTool, + sportmonksMotorsportGetFixturesByIdsTool, sportmonksMotorsportGetFixtureTool, + sportmonksMotorsportGetLapsByFixtureAndDriverTool, + sportmonksMotorsportGetLapsByFixtureAndLapTool, sportmonksMotorsportGetLapsByFixtureTool, + sportmonksMotorsportGetLatestLapsByFixtureTool, + sportmonksMotorsportGetLatestPitstopsByFixtureTool, + sportmonksMotorsportGetLatestStintsByFixtureTool, + sportmonksMotorsportGetLatestUpdatedDriversTool, + sportmonksMotorsportGetLatestUpdatedFixturesTool, + sportmonksMotorsportGetLeaguesByCountryTool, + sportmonksMotorsportGetLeaguesByDateTool, + sportmonksMotorsportGetLeaguesByLiveTool, + sportmonksMotorsportGetLeaguesByTeamTool, + sportmonksMotorsportGetLeaguesTool, + sportmonksMotorsportGetLeagueTool, sportmonksMotorsportGetLivescoresTool, + sportmonksMotorsportGetPitstopsByFixtureAndDriverTool, + sportmonksMotorsportGetPitstopsByFixtureAndLapTool, sportmonksMotorsportGetPitstopsByFixtureTool, + sportmonksMotorsportGetRaceResultsBySeasonAndDriverTool, + sportmonksMotorsportGetRaceResultsBySeasonAndTeamTool, + sportmonksMotorsportGetSchedulesBySeasonTool, + sportmonksMotorsportGetSeasonsTool, + sportmonksMotorsportGetSeasonTool, + sportmonksMotorsportGetStagesBySeasonTool, + sportmonksMotorsportGetStagesTool, + sportmonksMotorsportGetStageTool, + sportmonksMotorsportGetStatesTool, + sportmonksMotorsportGetStateTool, + sportmonksMotorsportGetStintsByFixtureAndDriverTool, + sportmonksMotorsportGetStintsByFixtureAndStintTool, + sportmonksMotorsportGetStintsByFixtureTool, sportmonksMotorsportGetTeamStandingsBySeasonTool, + sportmonksMotorsportGetTeamStandingsTool, + sportmonksMotorsportGetTeamsByCountryTool, + sportmonksMotorsportGetTeamsBySeasonTool, sportmonksMotorsportGetTeamsTool, sportmonksMotorsportGetTeamTool, + sportmonksMotorsportGetVenuesBySeasonTool, + sportmonksMotorsportGetVenuesTool, + sportmonksMotorsportGetVenueTool, sportmonksMotorsportSearchDriversTool, + sportmonksMotorsportSearchLeaguesTool, + sportmonksMotorsportSearchStagesTool, + sportmonksMotorsportSearchTeamsTool, + sportmonksMotorsportSearchVenuesTool, } from '@/tools/sportmonks_motorsport' import { + sportmonksOddsGetAllHistoricalOddsTool, + sportmonksOddsGetAllInplayOddsTool, + sportmonksOddsGetAllPreMatchOddsTool, + sportmonksOddsGetAllPremiumOddsTool, + sportmonksOddsGetBookmakerEventIdsByFixtureTool, + sportmonksOddsGetBookmakersByFixtureTool, sportmonksOddsGetBookmakersTool, sportmonksOddsGetBookmakerTool, + sportmonksOddsGetInplayOddsByFixtureAndBookmakerTool, + sportmonksOddsGetInplayOddsByFixtureAndMarketTool, sportmonksOddsGetInplayOddsByFixtureTool, + sportmonksOddsGetLastUpdatedInplayOddsTool, + sportmonksOddsGetLastUpdatedPreMatchOddsTool, sportmonksOddsGetMarketsTool, sportmonksOddsGetMarketTool, + sportmonksOddsGetPreMatchOddsByFixtureAndBookmakerTool, + sportmonksOddsGetPreMatchOddsByFixtureAndMarketTool, sportmonksOddsGetPreMatchOddsByFixtureTool, + sportmonksOddsGetPremiumOddsByFixtureAndBookmakerTool, + sportmonksOddsGetPremiumOddsByFixtureAndMarketTool, + sportmonksOddsGetPremiumOddsByFixtureTool, + sportmonksOddsGetUpdatedHistoricalOddsBetweenTool, + sportmonksOddsGetUpdatedPremiumOddsBetweenTool, sportmonksOddsSearchBookmakersTool, sportmonksOddsSearchMarketsTool, } from '@/tools/sportmonks_odds' @@ -4255,56 +4428,253 @@ export const tools: Record = { sendgrid_delete_template: sendGridDeleteTemplateTool, sendgrid_create_template_version: sendGridCreateTemplateVersionTool, smtp_send_mail: smtpSendMailTool, + sportmonks_football_expected_by_player: sportmonksExpectedByPlayerTool, + sportmonks_football_expected_by_team: sportmonksExpectedByTeamTool, + sportmonks_football_get_all_commentaries: sportmonksGetAllCommentariesTool, + sportmonks_football_get_all_fixtures: sportmonksGetAllFixturesTool, + sportmonks_football_get_all_players: sportmonksGetAllPlayersTool, + sportmonks_football_get_all_rivals: sportmonksGetAllRivalsTool, + sportmonks_football_get_all_teams: sportmonksGetAllTeamsTool, + sportmonks_football_get_all_transfer_rumours: sportmonksGetAllTransferRumoursTool, + sportmonks_football_get_all_transfers: sportmonksGetAllTransfersTool, + sportmonks_football_get_brackets_by_season: sportmonksGetBracketsBySeasonTool, + sportmonks_football_get_coach: sportmonksGetCoachTool, + sportmonks_football_get_coaches: sportmonksGetCoachesTool, + sportmonks_football_get_coaches_by_country: sportmonksGetCoachesByCountryTool, + sportmonks_football_get_commentaries_by_fixture: sportmonksGetCommentariesByFixtureTool, + sportmonks_football_get_current_leagues_by_team: sportmonksGetCurrentLeaguesByTeamTool, + sportmonks_football_get_expected_lineups_by_player: sportmonksGetExpectedLineupsByPlayerTool, + sportmonks_football_get_expected_lineups_by_team: sportmonksGetExpectedLineupsByTeamTool, + sportmonks_football_get_extended_team_squad: sportmonksGetExtendedTeamSquadTool, + sportmonks_football_get_fixture: sportmonksGetFixtureTool, sportmonks_football_get_fixtures_by_date: sportmonksGetFixturesByDateTool, sportmonks_football_get_fixtures_by_date_range: sportmonksGetFixturesByDateRangeTool, - sportmonks_football_get_fixture: sportmonksGetFixtureTool, + sportmonks_football_get_fixtures_by_date_range_for_team: + sportmonksGetFixturesByDateRangeForTeamTool, + sportmonks_football_get_fixtures_by_ids: sportmonksGetFixturesByIdsTool, + sportmonks_football_get_grouped_standings_by_round: sportmonksGetGroupedStandingsByRoundTool, sportmonks_football_get_head_to_head: sportmonksGetHeadToHeadTool, - sportmonks_football_get_livescores: sportmonksGetLivescoresTool, sportmonks_football_get_inplay_livescores: sportmonksGetInplayLivescoresTool, - sportmonks_football_get_leagues: sportmonksGetLeaguesTool, + sportmonks_football_get_latest_coaches: sportmonksGetLatestCoachesTool, + sportmonks_football_get_latest_fixtures: sportmonksGetLatestFixturesTool, + sportmonks_football_get_latest_livescores: sportmonksGetLatestLivescoresTool, + sportmonks_football_get_latest_players: sportmonksGetLatestPlayersTool, + sportmonks_football_get_latest_totw: sportmonksGetLatestTotwTool, + sportmonks_football_get_latest_transfers: sportmonksGetLatestTransfersTool, sportmonks_football_get_league: sportmonksGetLeagueTool, - sportmonks_football_search_teams: sportmonksSearchTeamsTool, - sportmonks_football_get_team: sportmonksGetTeamTool, - sportmonks_football_get_team_squad: sportmonksGetTeamSquadTool, - sportmonks_football_search_players: sportmonksSearchPlayersTool, + sportmonks_football_get_leagues: sportmonksGetLeaguesTool, + sportmonks_football_get_leagues_by_country: sportmonksGetLeaguesByCountryTool, + sportmonks_football_get_leagues_by_date: sportmonksGetLeaguesByDateTool, + sportmonks_football_get_leagues_by_team: sportmonksGetLeaguesByTeamTool, + sportmonks_football_get_live_leagues: sportmonksGetLiveLeaguesTool, + sportmonks_football_get_live_probabilities: sportmonksGetLiveProbabilitiesTool, + sportmonks_football_get_live_probabilities_by_fixture: + sportmonksGetLiveProbabilitiesByFixtureTool, + sportmonks_football_get_live_standings_by_league: sportmonksGetLiveStandingsByLeagueTool, + sportmonks_football_get_livescores: sportmonksGetLivescoresTool, + sportmonks_football_get_match_facts: sportmonksGetMatchFactsTool, + sportmonks_football_get_match_facts_by_date_range: sportmonksGetMatchFactsByDateRangeTool, + sportmonks_football_get_match_facts_by_fixture: sportmonksGetMatchFactsByFixtureTool, + sportmonks_football_get_match_facts_by_league: sportmonksGetMatchFactsByLeagueTool, + sportmonks_football_get_past_fixtures_by_tv_station: sportmonksGetPastFixturesByTvStationTool, sportmonks_football_get_player: sportmonksGetPlayerTool, + sportmonks_football_get_players_by_country: sportmonksGetPlayersByCountryTool, + sportmonks_football_get_postmatch_news: sportmonksGetPostmatchNewsTool, + sportmonks_football_get_postmatch_news_by_season: sportmonksGetPostmatchNewsBySeasonTool, + sportmonks_football_get_predictability_by_league: sportmonksGetPredictabilityByLeagueTool, + sportmonks_football_get_prematch_news: sportmonksGetPrematchNewsTool, + sportmonks_football_get_prematch_news_by_season: sportmonksGetPrematchNewsBySeasonTool, + sportmonks_football_get_prematch_news_upcoming: sportmonksGetPrematchNewsUpcomingTool, + sportmonks_football_get_probabilities: sportmonksGetProbabilitiesTool, + sportmonks_football_get_probabilities_by_fixture: sportmonksGetProbabilitiesByFixtureTool, + sportmonks_football_get_referee: sportmonksGetRefereeTool, + sportmonks_football_get_referees: sportmonksGetRefereesTool, + sportmonks_football_get_referees_by_country: sportmonksGetRefereesByCountryTool, + sportmonks_football_get_referees_by_season: sportmonksGetRefereesBySeasonTool, + sportmonks_football_get_rivals_by_team: sportmonksGetRivalsByTeamTool, + sportmonks_football_get_round: sportmonksGetRoundTool, + sportmonks_football_get_round_statistics: sportmonksGetRoundStatisticsTool, + sportmonks_football_get_rounds: sportmonksGetRoundsTool, + sportmonks_football_get_rounds_by_season: sportmonksGetRoundsBySeasonTool, + sportmonks_football_get_schedules_by_season: sportmonksGetSchedulesBySeasonTool, + sportmonks_football_get_schedules_by_season_and_team: sportmonksGetSchedulesBySeasonAndTeamTool, + sportmonks_football_get_schedules_by_team: sportmonksGetSchedulesByTeamTool, + sportmonks_football_get_season: sportmonksGetSeasonTool, + sportmonks_football_get_seasons: sportmonksGetSeasonsTool, + sportmonks_football_get_seasons_by_team: sportmonksGetSeasonsByTeamTool, + sportmonks_football_get_stage: sportmonksGetStageTool, + sportmonks_football_get_stage_statistics: sportmonksGetStageStatisticsTool, + sportmonks_football_get_stages: sportmonksGetStagesTool, + sportmonks_football_get_stages_by_season: sportmonksGetStagesBySeasonTool, + sportmonks_football_get_standing_corrections_by_season: + sportmonksGetStandingCorrectionsBySeasonTool, + sportmonks_football_get_standings: sportmonksGetStandingsTool, + sportmonks_football_get_standings_by_round: sportmonksGetStandingsByRoundTool, sportmonks_football_get_standings_by_season: sportmonksGetStandingsBySeasonTool, + sportmonks_football_get_state: sportmonksGetStateTool, + sportmonks_football_get_states: sportmonksGetStatesTool, + sportmonks_football_get_team: sportmonksGetTeamTool, + sportmonks_football_get_team_rankings: sportmonksGetTeamRankingsTool, + sportmonks_football_get_team_rankings_by_date: sportmonksGetTeamRankingsByDateTool, + sportmonks_football_get_team_rankings_by_team: sportmonksGetTeamRankingsByTeamTool, + sportmonks_football_get_team_squad: sportmonksGetTeamSquadTool, + sportmonks_football_get_team_squad_by_season: sportmonksGetTeamSquadBySeasonTool, + sportmonks_football_get_teams_by_country: sportmonksGetTeamsByCountryTool, + sportmonks_football_get_teams_by_season: sportmonksGetTeamsBySeasonTool, sportmonks_football_get_topscorers_by_season: sportmonksGetTopscorersBySeasonTool, - sportmonks_core_get_continents: sportmonksCoreGetContinentsTool, - sportmonks_core_get_continent: sportmonksCoreGetContinentTool, - sportmonks_core_get_countries: sportmonksCoreGetCountriesTool, - sportmonks_core_get_country: sportmonksCoreGetCountryTool, - sportmonks_core_search_countries: sportmonksCoreSearchCountriesTool, - sportmonks_core_get_regions: sportmonksCoreGetRegionsTool, - sportmonks_core_get_region: sportmonksCoreGetRegionTool, - sportmonks_core_get_cities: sportmonksCoreGetCitiesTool, - sportmonks_core_get_city: sportmonksCoreGetCityTool, - sportmonks_core_search_cities: sportmonksCoreSearchCitiesTool, - sportmonks_core_get_types: sportmonksCoreGetTypesTool, - sportmonks_core_get_type: sportmonksCoreGetTypeTool, - sportmonks_core_get_timezones: sportmonksCoreGetTimezonesTool, - sportmonks_motorsport_get_livescores: sportmonksMotorsportGetLivescoresTool, - sportmonks_motorsport_get_fixtures_by_date: sportmonksMotorsportGetFixturesByDateTool, - sportmonks_motorsport_get_fixture: sportmonksMotorsportGetFixtureTool, - sportmonks_motorsport_get_drivers: sportmonksMotorsportGetDriversTool, + sportmonks_football_get_topscorers_by_stage: sportmonksGetTopscorersByStageTool, + sportmonks_football_get_totw: sportmonksGetTotwTool, + sportmonks_football_get_totw_by_round: sportmonksGetTotwByRoundTool, + sportmonks_football_get_transfer: sportmonksGetTransferTool, + sportmonks_football_get_transfer_rumour: sportmonksGetTransferRumourTool, + sportmonks_football_get_transfer_rumours_between_dates: + sportmonksGetTransferRumoursBetweenDatesTool, + sportmonks_football_get_transfer_rumours_by_player: sportmonksGetTransferRumoursByPlayerTool, + sportmonks_football_get_transfer_rumours_by_team: sportmonksGetTransferRumoursByTeamTool, + sportmonks_football_get_transfers_between_dates: sportmonksGetTransfersBetweenDatesTool, + sportmonks_football_get_transfers_by_player: sportmonksGetTransfersByPlayerTool, + sportmonks_football_get_transfers_by_team: sportmonksGetTransfersByTeamTool, + sportmonks_football_get_tv_station: sportmonksGetTvStationTool, + sportmonks_football_get_tv_stations: sportmonksGetTvStationsTool, + sportmonks_football_get_tv_stations_by_fixture: sportmonksGetTvStationsByFixtureTool, + sportmonks_football_get_upcoming_fixtures_by_market: sportmonksGetUpcomingFixturesByMarketTool, + sportmonks_football_get_upcoming_fixtures_by_tv_station: + sportmonksGetUpcomingFixturesByTvStationTool, + sportmonks_football_get_value_bets: sportmonksGetValueBetsTool, + sportmonks_football_get_value_bets_by_fixture: sportmonksGetValueBetsByFixtureTool, + sportmonks_football_get_venue: sportmonksGetVenueTool, + sportmonks_football_get_venues: sportmonksGetVenuesTool, + sportmonks_football_get_venues_by_season: sportmonksGetVenuesBySeasonTool, + sportmonks_football_search_coaches: sportmonksSearchCoachesTool, + sportmonks_football_search_fixtures: sportmonksSearchFixturesTool, + sportmonks_football_search_leagues: sportmonksSearchLeaguesTool, + sportmonks_football_search_players: sportmonksSearchPlayersTool, + sportmonks_football_search_referees: sportmonksSearchRefereesTool, + sportmonks_football_search_rounds: sportmonksSearchRoundsTool, + sportmonks_football_search_seasons: sportmonksSearchSeasonsTool, + sportmonks_football_search_stages: sportmonksSearchStagesTool, + sportmonks_football_search_teams: sportmonksSearchTeamsTool, + sportmonks_football_search_venues: sportmonksSearchVenuesTool, + sportmonks_motorsport_get_all_fixtures: sportmonksMotorsportGetAllFixturesTool, + sportmonks_motorsport_get_current_leagues_by_team: + sportmonksMotorsportGetCurrentLeaguesByTeamTool, sportmonks_motorsport_get_driver: sportmonksMotorsportGetDriverTool, - sportmonks_motorsport_search_drivers: sportmonksMotorsportSearchDriversTool, - sportmonks_motorsport_get_teams: sportmonksMotorsportGetTeamsTool, - sportmonks_motorsport_get_team: sportmonksMotorsportGetTeamTool, + sportmonks_motorsport_get_driver_standings: sportmonksMotorsportGetDriverStandingsTool, sportmonks_motorsport_get_driver_standings_by_season: sportmonksMotorsportGetDriverStandingsBySeasonTool, - sportmonks_motorsport_get_team_standings_by_season: - sportmonksMotorsportGetTeamStandingsBySeasonTool, + sportmonks_motorsport_get_drivers: sportmonksMotorsportGetDriversTool, + sportmonks_motorsport_get_drivers_by_country: sportmonksMotorsportGetDriversByCountryTool, + sportmonks_motorsport_get_drivers_by_season: sportmonksMotorsportGetDriversBySeasonTool, + sportmonks_motorsport_get_fixture: sportmonksMotorsportGetFixtureTool, + sportmonks_motorsport_get_fixtures_by_date: sportmonksMotorsportGetFixturesByDateTool, + sportmonks_motorsport_get_fixtures_by_date_range: sportmonksMotorsportGetFixturesByDateRangeTool, + sportmonks_motorsport_get_fixtures_by_ids: sportmonksMotorsportGetFixturesByIdsTool, sportmonks_motorsport_get_laps_by_fixture: sportmonksMotorsportGetLapsByFixtureTool, + sportmonks_motorsport_get_laps_by_fixture_and_driver: + sportmonksMotorsportGetLapsByFixtureAndDriverTool, + sportmonks_motorsport_get_laps_by_fixture_and_lap: sportmonksMotorsportGetLapsByFixtureAndLapTool, + sportmonks_motorsport_get_latest_laps_by_fixture: sportmonksMotorsportGetLatestLapsByFixtureTool, + sportmonks_motorsport_get_latest_pitstops_by_fixture: + sportmonksMotorsportGetLatestPitstopsByFixtureTool, + sportmonks_motorsport_get_latest_stints_by_fixture: + sportmonksMotorsportGetLatestStintsByFixtureTool, + sportmonks_motorsport_get_latest_updated_drivers: sportmonksMotorsportGetLatestUpdatedDriversTool, + sportmonks_motorsport_get_latest_updated_fixtures: + sportmonksMotorsportGetLatestUpdatedFixturesTool, + sportmonks_motorsport_get_league: sportmonksMotorsportGetLeagueTool, + sportmonks_motorsport_get_leagues: sportmonksMotorsportGetLeaguesTool, + sportmonks_motorsport_get_leagues_by_country: sportmonksMotorsportGetLeaguesByCountryTool, + sportmonks_motorsport_get_leagues_by_date: sportmonksMotorsportGetLeaguesByDateTool, + sportmonks_motorsport_get_leagues_by_live: sportmonksMotorsportGetLeaguesByLiveTool, + sportmonks_motorsport_get_leagues_by_team: sportmonksMotorsportGetLeaguesByTeamTool, + sportmonks_motorsport_get_livescores: sportmonksMotorsportGetLivescoresTool, sportmonks_motorsport_get_pitstops_by_fixture: sportmonksMotorsportGetPitstopsByFixtureTool, - sportmonks_odds_get_pre_match_odds_by_fixture: sportmonksOddsGetPreMatchOddsByFixtureTool, - sportmonks_odds_get_inplay_odds_by_fixture: sportmonksOddsGetInplayOddsByFixtureTool, - sportmonks_odds_get_bookmakers: sportmonksOddsGetBookmakersTool, + sportmonks_motorsport_get_pitstops_by_fixture_and_driver: + sportmonksMotorsportGetPitstopsByFixtureAndDriverTool, + sportmonks_motorsport_get_pitstops_by_fixture_and_lap: + sportmonksMotorsportGetPitstopsByFixtureAndLapTool, + sportmonks_motorsport_get_race_results_by_season_and_driver: + sportmonksMotorsportGetRaceResultsBySeasonAndDriverTool, + sportmonks_motorsport_get_race_results_by_season_and_team: + sportmonksMotorsportGetRaceResultsBySeasonAndTeamTool, + sportmonks_motorsport_get_schedules_by_season: sportmonksMotorsportGetSchedulesBySeasonTool, + sportmonks_motorsport_get_season: sportmonksMotorsportGetSeasonTool, + sportmonks_motorsport_get_seasons: sportmonksMotorsportGetSeasonsTool, + sportmonks_motorsport_get_stage: sportmonksMotorsportGetStageTool, + sportmonks_motorsport_get_stages: sportmonksMotorsportGetStagesTool, + sportmonks_motorsport_get_stages_by_season: sportmonksMotorsportGetStagesBySeasonTool, + sportmonks_motorsport_get_state: sportmonksMotorsportGetStateTool, + sportmonks_motorsport_get_states: sportmonksMotorsportGetStatesTool, + sportmonks_motorsport_get_stints_by_fixture: sportmonksMotorsportGetStintsByFixtureTool, + sportmonks_motorsport_get_stints_by_fixture_and_driver: + sportmonksMotorsportGetStintsByFixtureAndDriverTool, + sportmonks_motorsport_get_stints_by_fixture_and_stint: + sportmonksMotorsportGetStintsByFixtureAndStintTool, + sportmonks_motorsport_get_team: sportmonksMotorsportGetTeamTool, + sportmonks_motorsport_get_team_standings: sportmonksMotorsportGetTeamStandingsTool, + sportmonks_motorsport_get_team_standings_by_season: + sportmonksMotorsportGetTeamStandingsBySeasonTool, + sportmonks_motorsport_get_teams: sportmonksMotorsportGetTeamsTool, + sportmonks_motorsport_get_teams_by_country: sportmonksMotorsportGetTeamsByCountryTool, + sportmonks_motorsport_get_teams_by_season: sportmonksMotorsportGetTeamsBySeasonTool, + sportmonks_motorsport_get_venue: sportmonksMotorsportGetVenueTool, + sportmonks_motorsport_get_venues: sportmonksMotorsportGetVenuesTool, + sportmonks_motorsport_get_venues_by_season: sportmonksMotorsportGetVenuesBySeasonTool, + sportmonks_motorsport_search_drivers: sportmonksMotorsportSearchDriversTool, + sportmonks_motorsport_search_leagues: sportmonksMotorsportSearchLeaguesTool, + sportmonks_motorsport_search_stages: sportmonksMotorsportSearchStagesTool, + sportmonks_motorsport_search_teams: sportmonksMotorsportSearchTeamsTool, + sportmonks_motorsport_search_venues: sportmonksMotorsportSearchVenuesTool, + sportmonks_odds_get_all_historical_odds: sportmonksOddsGetAllHistoricalOddsTool, + sportmonks_odds_get_all_inplay_odds: sportmonksOddsGetAllInplayOddsTool, + sportmonks_odds_get_all_pre_match_odds: sportmonksOddsGetAllPreMatchOddsTool, + sportmonks_odds_get_all_premium_odds: sportmonksOddsGetAllPremiumOddsTool, sportmonks_odds_get_bookmaker: sportmonksOddsGetBookmakerTool, - sportmonks_odds_search_bookmakers: sportmonksOddsSearchBookmakersTool, - sportmonks_odds_get_markets: sportmonksOddsGetMarketsTool, + sportmonks_odds_get_bookmaker_event_ids_by_fixture: + sportmonksOddsGetBookmakerEventIdsByFixtureTool, + sportmonks_odds_get_bookmakers: sportmonksOddsGetBookmakersTool, + sportmonks_odds_get_bookmakers_by_fixture: sportmonksOddsGetBookmakersByFixtureTool, + sportmonks_odds_get_inplay_odds_by_fixture: sportmonksOddsGetInplayOddsByFixtureTool, + sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker: + sportmonksOddsGetInplayOddsByFixtureAndBookmakerTool, + sportmonks_odds_get_inplay_odds_by_fixture_and_market: + sportmonksOddsGetInplayOddsByFixtureAndMarketTool, + sportmonks_odds_get_last_updated_inplay_odds: sportmonksOddsGetLastUpdatedInplayOddsTool, + sportmonks_odds_get_last_updated_pre_match_odds: sportmonksOddsGetLastUpdatedPreMatchOddsTool, sportmonks_odds_get_market: sportmonksOddsGetMarketTool, + sportmonks_odds_get_markets: sportmonksOddsGetMarketsTool, + sportmonks_odds_get_pre_match_odds_by_fixture: sportmonksOddsGetPreMatchOddsByFixtureTool, + sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker: + sportmonksOddsGetPreMatchOddsByFixtureAndBookmakerTool, + sportmonks_odds_get_pre_match_odds_by_fixture_and_market: + sportmonksOddsGetPreMatchOddsByFixtureAndMarketTool, + sportmonks_odds_get_premium_odds_by_fixture: sportmonksOddsGetPremiumOddsByFixtureTool, + sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker: + sportmonksOddsGetPremiumOddsByFixtureAndBookmakerTool, + sportmonks_odds_get_premium_odds_by_fixture_and_market: + sportmonksOddsGetPremiumOddsByFixtureAndMarketTool, + sportmonks_odds_get_updated_historical_odds_between: + sportmonksOddsGetUpdatedHistoricalOddsBetweenTool, + sportmonks_odds_get_updated_premium_odds_between: sportmonksOddsGetUpdatedPremiumOddsBetweenTool, + sportmonks_odds_search_bookmakers: sportmonksOddsSearchBookmakersTool, sportmonks_odds_search_markets: sportmonksOddsSearchMarketsTool, + sportmonks_core_get_cities: sportmonksCoreGetCitiesTool, + sportmonks_core_get_city: sportmonksCoreGetCityTool, + sportmonks_core_get_continent: sportmonksCoreGetContinentTool, + sportmonks_core_get_continents: sportmonksCoreGetContinentsTool, + sportmonks_core_get_countries: sportmonksCoreGetCountriesTool, + sportmonks_core_get_country: sportmonksCoreGetCountryTool, + sportmonks_core_get_entity_filters: sportmonksCoreGetEntityFiltersTool, + sportmonks_core_get_my_usage: sportmonksCoreGetMyUsageTool, + sportmonks_core_get_region: sportmonksCoreGetRegionTool, + sportmonks_core_get_regions: sportmonksCoreGetRegionsTool, + sportmonks_core_get_timezones: sportmonksCoreGetTimezonesTool, + sportmonks_core_get_type: sportmonksCoreGetTypeTool, + sportmonks_core_get_type_by_entity: sportmonksCoreGetTypeByEntityTool, + sportmonks_core_get_types: sportmonksCoreGetTypesTool, + sportmonks_core_search_cities: sportmonksCoreSearchCitiesTool, + sportmonks_core_search_countries: sportmonksCoreSearchCountriesTool, + sportmonks_core_search_regions: sportmonksCoreSearchRegionsTool, sftp_upload: sftpUploadTool, sftp_download: sftpDownloadTool, sftp_list: sftpListTool, diff --git a/apps/sim/tools/sportmonks_core/get_continents.ts b/apps/sim/tools/sportmonks_core/get_continents.ts index 54c39b29452..bc0c2a59ca4 100644 --- a/apps/sim/tools/sportmonks_core/get_continents.ts +++ b/apps/sim/tools/sportmonks_core/get_continents.ts @@ -47,12 +47,6 @@ export const sportmonksCoreGetContinentsTool: ToolConfig< visibility: 'user-or-llm', description: 'Semicolon-separated relations to enrich the response (e.g. countries)', }, - filters: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Filters to apply', - }, per_page: { type: 'string', required: false, diff --git a/apps/sim/tools/sportmonks_core/get_entity_filters.ts b/apps/sim/tools/sportmonks_core/get_entity_filters.ts new file mode 100644 index 00000000000..66b5791bc78 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_entity_filters.ts @@ -0,0 +1,61 @@ +import { + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_MY_BASE_URL, type SportmonksEntityFilters } from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetEntityFiltersParams extends SportmonksBaseParams {} + +export interface SportmonksGetEntityFiltersResponse extends ToolResponse { + output: { + entityFilters: SportmonksEntityFilters | null + } +} + +export const sportmonksCoreGetEntityFiltersTool: ToolConfig< + SportmonksGetEntityFiltersParams, + SportmonksGetEntityFiltersResponse +> = { + id: 'sportmonks_core_get_entity_filters', + name: 'Get All Entity Filters', + description: 'Retrieve all available filters grouped per entity from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + }, + + request: { + url: () => `${SPORTMONKS_MY_BASE_URL}/filters/entity`, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_entity_filters') + } + return { + success: true, + output: { + entityFilters: data.data ?? null, + }, + } + }, + + outputs: { + entityFilters: { + type: 'json', + description: + 'Map of entity name to its available filter names, e.g. {fixture: ["fixtureLeagues", "fixtureSeasons"], event: ["eventTypes"]}', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_my_usage.ts b/apps/sim/tools/sportmonks_core/get_my_usage.ts new file mode 100644 index 00000000000..fc776e34371 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_my_usage.ts @@ -0,0 +1,92 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MY_BASE_URL, + SPORTMONKS_USAGE_PROPERTIES, + type SportmonksUsage, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMyUsageParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetMyUsageResponse extends ToolResponse { + output: { + usage: SportmonksUsage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreGetMyUsageTool: ToolConfig< + SportmonksGetMyUsageParams, + SportmonksGetMyUsageResponse +> = { + id: 'sportmonks_core_get_my_usage', + name: 'Get My Usage', + description: 'Retrieve your Sportmonks API usage aggregated per 5 minutes', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MY_BASE_URL}/usage`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_my_usage') + } + return { + success: true, + output: { + usage: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + usage: { + type: 'array', + description: 'Array of API usage records aggregated per 5-minute period', + items: { type: 'object', properties: SPORTMONKS_USAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/get_type_by_entity.ts b/apps/sim/tools/sportmonks_core/get_type_by_entity.ts new file mode 100644 index 00000000000..58f882ab5b4 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/get_type_by_entity.ts @@ -0,0 +1,64 @@ +import { + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + type SportmonksTypesByEntity, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTypeByEntityParams extends SportmonksBaseParams {} + +export interface SportmonksGetTypeByEntityResponse extends ToolResponse { + output: { + typesByEntity: SportmonksTypesByEntity | null + } +} + +export const sportmonksCoreGetTypeByEntityTool: ToolConfig< + SportmonksGetTypeByEntityParams, + SportmonksGetTypeByEntityResponse +> = { + id: 'sportmonks_core_get_type_by_entity', + name: 'Get Type by Entity', + description: 'Retrieve the available types grouped per entity from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + }, + + request: { + url: () => `${SPORTMONKS_CORE_BASE_URL}/types/entities`, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_type_by_entity') + } + return { + success: true, + output: { + typesByEntity: data.data ?? null, + }, + } + }, + + outputs: { + typesByEntity: { + type: 'json', + description: + 'Map of entity name to its available types, e.g. {CoachStatisticDetail: {updated_at, types: [{id, name, code, developer_name, model_type, stat_group}]}}', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_core/index.ts b/apps/sim/tools/sportmonks_core/index.ts index 1074d060463..04bd83ac30b 100644 --- a/apps/sim/tools/sportmonks_core/index.ts +++ b/apps/sim/tools/sportmonks_core/index.ts @@ -4,10 +4,14 @@ export { sportmonksCoreGetContinentTool } from './get_continent' export { sportmonksCoreGetContinentsTool } from './get_continents' export { sportmonksCoreGetCountriesTool } from './get_countries' export { sportmonksCoreGetCountryTool } from './get_country' +export { sportmonksCoreGetEntityFiltersTool } from './get_entity_filters' +export { sportmonksCoreGetMyUsageTool } from './get_my_usage' export { sportmonksCoreGetRegionTool } from './get_region' export { sportmonksCoreGetRegionsTool } from './get_regions' export { sportmonksCoreGetTimezonesTool } from './get_timezones' export { sportmonksCoreGetTypeTool } from './get_type' +export { sportmonksCoreGetTypeByEntityTool } from './get_type_by_entity' export { sportmonksCoreGetTypesTool } from './get_types' export { sportmonksCoreSearchCitiesTool } from './search_cities' export { sportmonksCoreSearchCountriesTool } from './search_countries' +export { sportmonksCoreSearchRegionsTool } from './search_regions' diff --git a/apps/sim/tools/sportmonks_core/search_cities.ts b/apps/sim/tools/sportmonks_core/search_cities.ts index 91d463a5c41..ecbef83ae53 100644 --- a/apps/sim/tools/sportmonks_core/search_cities.ts +++ b/apps/sim/tools/sportmonks_core/search_cities.ts @@ -55,6 +55,12 @@ export const sportmonksCoreSearchCitiesTool: ToolConfig< visibility: 'user-or-llm', description: 'Semicolon-separated relations to enrich the response (e.g. region)', }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, per_page: { type: 'string', required: false, @@ -67,6 +73,12 @@ export const sportmonksCoreSearchCitiesTool: ToolConfig< visibility: 'user-or-llm', description: 'Page number to retrieve', }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, }, request: { diff --git a/apps/sim/tools/sportmonks_core/search_countries.ts b/apps/sim/tools/sportmonks_core/search_countries.ts index cf3fc3101b4..6545c068eed 100644 --- a/apps/sim/tools/sportmonks_core/search_countries.ts +++ b/apps/sim/tools/sportmonks_core/search_countries.ts @@ -53,7 +53,13 @@ export const sportmonksCoreSearchCountriesTool: ToolConfig< type: 'string', required: false, visibility: 'user-or-llm', - description: 'Semicolon-separated relations to enrich the response (e.g. continent)', + description: 'Semicolon-separated relations to enrich the response (e.g. continent;regions)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', }, per_page: { type: 'string', @@ -67,6 +73,12 @@ export const sportmonksCoreSearchCountriesTool: ToolConfig< visibility: 'user-or-llm', description: 'Page number to retrieve', }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, }, request: { diff --git a/apps/sim/tools/sportmonks_core/search_regions.ts b/apps/sim/tools/sportmonks_core/search_regions.ts new file mode 100644 index 00000000000..7ff009da784 --- /dev/null +++ b/apps/sim/tools/sportmonks_core/search_regions.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_CORE_BASE_URL, + SPORTMONKS_REGION_PROPERTIES, + type SportmonksRegion, +} from '@/tools/sportmonks_core/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchRegionsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchRegionsResponse extends ToolResponse { + output: { + regions: SportmonksRegion[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksCoreSearchRegionsTool: ToolConfig< + SportmonksSearchRegionsParams, + SportmonksSearchRegionsResponse +> = { + id: 'sportmonks_core_search_regions', + name: 'Search Regions', + description: 'Search for regions by name from the Sportmonks Core API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The region name to search for (e.g. Utrecht)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;cities)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_CORE_BASE_URL}/regions/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_regions') + } + return { + success: true, + output: { + regions: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + regions: { + type: 'array', + description: 'Array of region objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_REGION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_core/types.ts b/apps/sim/tools/sportmonks_core/types.ts index 08dd316f083..2852a80d8ea 100644 --- a/apps/sim/tools/sportmonks_core/types.ts +++ b/apps/sim/tools/sportmonks_core/types.ts @@ -6,6 +6,14 @@ import type { OutputProperty } from '@/tools/types' */ export const SPORTMONKS_CORE_BASE_URL = 'https://api.sportmonks.com/v3/core' +/** + * Base URL for the Sportmonks "My Sportmonks" endpoints (account/subscription + * scoped data such as entity filters and API usage). These live under `/v3/my` + * rather than `/v3/core` but are documented as part of the Core API. + * @see https://docs.sportmonks.com/v3/core-api/my-sportmonks/get-my-usage + */ +export const SPORTMONKS_MY_BASE_URL = 'https://api.sportmonks.com/v3/my' + /** * Output property definitions for a Continent object. * @see https://docs.sportmonks.com/v3/core-api/entities/core @@ -78,7 +86,12 @@ export const SPORTMONKS_REGION_PROPERTIES = { export const SPORTMONKS_CITY_PROPERTIES = { id: { type: 'number', description: 'Unique id of the city' }, country_id: { type: 'number', description: 'Country of the city' }, - region: { type: 'number', description: 'Region of the city', nullable: true, optional: true }, + region_id: { + type: 'number', + description: 'Region id of the city', + nullable: true, + optional: true, + }, name: { type: 'string', description: 'Name of the city' }, latitude: { type: 'string', description: 'Latitude of the city', nullable: true, optional: true }, longitude: { @@ -124,6 +137,29 @@ export const SPORTMONKS_TYPE_PROPERTIES = { }, } as const satisfies Record +/** + * Output property definitions for a My Sportmonks API usage record. + * @see https://docs.sportmonks.com/v3/core-api/my-sportmonks/get-my-usage + */ +export const SPORTMONKS_USAGE_PROPERTIES = { + id: { type: 'number', description: 'Identifier of the usage record' }, + endpoint: { type: 'string', description: 'Identifier of the requested endpoint' }, + count: { type: 'number', description: 'Total calls for the given timeframe' }, + entity: { type: 'string', description: 'The entity the rate limit applies on' }, + remaining_requests: { + type: 'number', + description: 'Amount of requests remaining for the entity in the hourly rate limit', + }, + period_start: { + type: 'number', + description: 'Timestamp representing the aggregation start time', + }, + period_end: { + type: 'number', + description: 'Timestamp representing the aggregation end time', + }, +} as const satisfies Record + export interface SportmonksContinent { id: number name: string @@ -154,7 +190,7 @@ export interface SportmonksRegion { export interface SportmonksCity { id: number country_id: number - region?: number | null + region_id?: number | null name: string latitude?: string | null longitude?: string | null @@ -170,3 +206,41 @@ export interface SportmonksType { group?: string | null description?: string | null } + +export interface SportmonksUsage { + id: number + endpoint: string + count: number + entity: string + remaining_requests: number + period_start: number + period_end: number +} + +/** A single type entry as returned by the "Type by Entity" endpoint. */ +export interface SportmonksTypeEntityEntry { + id: number + name: string + code?: string | null + developer_name?: string | null + model_type?: string | null + stat_group?: string | null +} + +/** The per-entity grouping returned by the "Type by Entity" endpoint. */ +export interface SportmonksTypeEntityGroup { + updated_at: string + types: SportmonksTypeEntityEntry[] +} + +/** + * Response shape of the "Type by Entity" endpoint: a map keyed by entity name + * (e.g. CoachStatisticDetail) to its available types. + */ +export type SportmonksTypesByEntity = Record + +/** + * Response shape of the "All Entity Filters" endpoint: a map keyed by entity + * name to the list of filter names available on that entity. + */ +export type SportmonksEntityFilters = Record diff --git a/apps/sim/tools/sportmonks_football/expected_by_player.ts b/apps/sim/tools/sportmonks_football/expected_by_player.ts new file mode 100644 index 00000000000..d09b3d61ad1 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/expected_by_player.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_EXPECTED_PLAYER_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksExpectedPlayer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksExpectedByPlayerParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksExpectedByPlayerResponse extends ToolResponse { + output: { + expected: SportmonksExpectedPlayer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksExpectedByPlayerTool: ToolConfig< + SportmonksExpectedByPlayerParams, + SportmonksExpectedByPlayerResponse +> = { + id: 'sportmonks_football_expected_by_player', + name: 'Get Expected xG by Player', + description: 'Retrieve lineup-level expected goals (xG) values per player from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. fixture;player;team;type)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/expected/lineups`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'expected_by_player') + } + return { + success: true, + output: { + expected: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + expected: { + type: 'array', + description: 'Array of player-level expected goals (xG) entries', + items: { type: 'object', properties: SPORTMONKS_EXPECTED_PLAYER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/expected_by_team.ts b/apps/sim/tools/sportmonks_football/expected_by_team.ts new file mode 100644 index 00000000000..772c9b5a37d --- /dev/null +++ b/apps/sim/tools/sportmonks_football/expected_by_team.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_EXPECTED_TEAM_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksExpectedTeam, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksExpectedByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksExpectedByTeamResponse extends ToolResponse { + output: { + expected: SportmonksExpectedTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksExpectedByTeamTool: ToolConfig< + SportmonksExpectedByTeamParams, + SportmonksExpectedByTeamResponse +> = { + id: 'sportmonks_football_expected_by_team', + name: 'Get Expected xG by Team', + description: 'Retrieve fixture-level expected goals (xG) values per team from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. fixture;participant;type)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/expected/fixtures`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'expected_by_team') + } + return { + success: true, + output: { + expected: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + expected: { + type: 'array', + description: 'Array of team-level expected goals (xG) entries', + items: { type: 'object', properties: SPORTMONKS_EXPECTED_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_commentaries.ts b/apps/sim/tools/sportmonks_football/get_all_commentaries.ts new file mode 100644 index 00000000000..ecd5c4436ce --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_commentaries.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COMMENTARY_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCommentary, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllCommentariesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllCommentariesResponse extends ToolResponse { + output: { + commentaries: SportmonksCommentary[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllCommentariesTool: ToolConfig< + SportmonksGetAllCommentariesParams, + SportmonksGetAllCommentariesResponse +> = { + id: 'sportmonks_football_get_all_commentaries', + name: 'Get All Commentaries', + description: 'Retrieve all textual commentaries available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixture;player)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/commentaries`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_commentaries') + } + return { + success: true, + output: { + commentaries: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + commentaries: { + type: 'array', + description: 'Array of commentary entries', + items: { type: 'object', properties: SPORTMONKS_COMMENTARY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_fixtures.ts b/apps/sim/tools/sportmonks_football/get_all_fixtures.ts new file mode 100644 index 00000000000..0a21b7fdcd5 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_fixtures.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllFixturesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllFixturesResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllFixturesTool: ToolConfig< + SportmonksGetAllFixturesParams, + SportmonksGetAllFixturesResponse +> = { + id: 'sportmonks_football_get_all_fixtures', + name: 'Get All Fixtures', + description: 'Retrieve all football fixtures available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_fixtures') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of fixture objects', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_players.ts b/apps/sim/tools/sportmonks_football/get_all_players.ts new file mode 100644 index 00000000000..98c96f0bcd0 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_players.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PLAYER_PROPERTIES, + type SportmonksPlayer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllPlayersParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllPlayersResponse extends ToolResponse { + output: { + players: SportmonksPlayer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllPlayersTool: ToolConfig< + SportmonksGetAllPlayersParams, + SportmonksGetAllPlayersResponse +> = { + id: 'sportmonks_football_get_all_players', + name: 'Get All Players', + description: 'Retrieve all football players available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. nationality;position)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order players by id (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/players`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_players') + } + return { + success: true, + output: { + players: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + players: { + type: 'array', + description: 'Array of player objects', + items: { type: 'object', properties: SPORTMONKS_PLAYER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_rivals.ts b/apps/sim/tools/sportmonks_football/get_all_rivals.ts new file mode 100644 index 00000000000..1837819aa31 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_rivals.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_RIVAL_PROPERTIES, + type SportmonksRival, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllRivalsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllRivalsResponse extends ToolResponse { + output: { + rivals: SportmonksRival[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllRivalsTool: ToolConfig< + SportmonksGetAllRivalsParams, + SportmonksGetAllRivalsResponse +> = { + id: 'sportmonks_football_get_all_rivals', + name: 'Get All Rivals', + description: 'Retrieve all teams with their rivals information from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. team;rival)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/rivals`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_rivals') + } + return { + success: true, + output: { + rivals: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + rivals: { + type: 'array', + description: 'Array of rival relationships', + items: { type: 'object', properties: SPORTMONKS_RIVAL_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_teams.ts b/apps/sim/tools/sportmonks_football/get_all_teams.ts new file mode 100644 index 00000000000..815b90f4980 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_teams.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_PROPERTIES, + type SportmonksTeam, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllTeamsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllTeamsResponse extends ToolResponse { + output: { + teams: SportmonksTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllTeamsTool: ToolConfig< + SportmonksGetAllTeamsParams, + SportmonksGetAllTeamsResponse +> = { + id: 'sportmonks_football_get_all_teams', + name: 'Get All Teams', + description: 'Retrieve all football teams available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;venue)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order teams by id (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/teams`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_teams') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team objects', + items: { type: 'object', properties: SPORTMONKS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_transfer_rumours.ts b/apps/sim/tools/sportmonks_football/get_all_transfer_rumours.ts new file mode 100644 index 00000000000..295d86a2483 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_transfer_rumours.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES, + type SportmonksTransferRumour, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllTransferRumoursParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllTransferRumoursResponse extends ToolResponse { + output: { + transferRumours: SportmonksTransferRumour[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllTransferRumoursTool: ToolConfig< + SportmonksGetAllTransferRumoursParams, + SportmonksGetAllTransferRumoursResponse +> = { + id: 'sportmonks_football_get_all_transfer_rumours', + name: 'Get All Transfer Rumours', + description: 'Retrieve all transfer rumours available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/transfer-rumours`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_transfer_rumours') + } + return { + success: true, + output: { + transferRumours: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transferRumours: { + type: 'array', + description: 'Array of transfer rumour objects', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_all_transfers.ts b/apps/sim/tools/sportmonks_football/get_all_transfers.ts new file mode 100644 index 00000000000..755290efe95 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_all_transfers.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_PROPERTIES, + type SportmonksTransfer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllTransfersParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllTransfersResponse extends ToolResponse { + output: { + transfers: SportmonksTransfer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetAllTransfersTool: ToolConfig< + SportmonksGetAllTransfersParams, + SportmonksGetAllTransfersResponse +> = { + id: 'sportmonks_football_get_all_transfers', + name: 'Get All Transfers', + description: 'Retrieve all transfers available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. transferTypes:219,220)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/transfers`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_transfers') + } + return { + success: true, + output: { + transfers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transfers: { + type: 'array', + description: 'Array of transfer objects', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_brackets_by_season.ts b/apps/sim/tools/sportmonks_football/get_brackets_by_season.ts new file mode 100644 index 00000000000..fda5f32582d --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_brackets_by_season.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_FOOTBALL_BASE_URL } from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetBracketsBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetBracketsBySeasonResponse extends ToolResponse { + output: { + brackets: Record | null + } +} + +export const sportmonksGetBracketsBySeasonTool: ToolConfig< + SportmonksGetBracketsBySeasonParams, + SportmonksGetBracketsBySeasonResponse +> = { + id: 'sportmonks_football_get_brackets_by_season', + name: 'Get Brackets by Season', + description: + 'Retrieve the knockout-stage tournament bracket (stages and progression edges) for a season ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/seasons/${encodeURIComponent(params.seasonId.trim())}/brackets` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_brackets_by_season') + } + return { + success: true, + output: { + brackets: data.data ?? null, + }, + } + }, + + outputs: { + brackets: { + type: 'json', + description: + 'Bracket object containing stages (fixtures grouped by knockout round) and edges (progression paths between fixtures)', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_coach.ts b/apps/sim/tools/sportmonks_football/get_coach.ts new file mode 100644 index 00000000000..d1c1d1a149e --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_coach.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COACH_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCoach, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCoachParams extends SportmonksBaseParams { + coachId: string +} + +export interface SportmonksGetCoachResponse extends ToolResponse { + output: { + coach: SportmonksCoach | null + } +} + +export const sportmonksGetCoachTool: ToolConfig< + SportmonksGetCoachParams, + SportmonksGetCoachResponse +> = { + id: 'sportmonks_football_get_coach', + name: 'Get Coach by ID', + description: 'Retrieve a single football coach by their ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + coachId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the coach', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;teams;statistics)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/coaches/${encodeURIComponent(params.coachId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_coach') + } + return { + success: true, + output: { + coach: data.data ?? null, + }, + } + }, + + outputs: { + coach: { + type: 'object', + description: 'The requested coach object', + properties: SPORTMONKS_COACH_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_coaches.ts b/apps/sim/tools/sportmonks_football/get_coaches.ts new file mode 100644 index 00000000000..d4c8f04394a --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_coaches.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COACH_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCoach, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCoachesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetCoachesResponse extends ToolResponse { + output: { + coaches: SportmonksCoach[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetCoachesTool: ToolConfig< + SportmonksGetCoachesParams, + SportmonksGetCoachesResponse +> = { + id: 'sportmonks_football_get_coaches', + name: 'Get Coaches', + description: 'Retrieve all football coaches available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. coachCountries:462)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/coaches`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_coaches') + } + return { + success: true, + output: { + coaches: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + coaches: { + type: 'array', + description: 'Array of coach objects', + items: { type: 'object', properties: SPORTMONKS_COACH_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_coaches_by_country.ts b/apps/sim/tools/sportmonks_football/get_coaches_by_country.ts new file mode 100644 index 00000000000..9fd95925921 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_coaches_by_country.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COACH_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCoach, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCoachesByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksGetCoachesByCountryResponse extends ToolResponse { + output: { + coaches: SportmonksCoach[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetCoachesByCountryTool: ToolConfig< + SportmonksGetCoachesByCountryParams, + SportmonksGetCoachesByCountryResponse +> = { + id: 'sportmonks_football_get_coaches_by_country', + name: 'Get Coaches by Country', + description: 'Retrieve all coaches for a country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;nationality)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/coaches/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_coaches_by_country') + } + return { + success: true, + output: { + coaches: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + coaches: { + type: 'array', + description: 'Array of coach objects for the country', + items: { type: 'object', properties: SPORTMONKS_COACH_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_commentaries_by_fixture.ts b/apps/sim/tools/sportmonks_football/get_commentaries_by_fixture.ts new file mode 100644 index 00000000000..bdff8a5a290 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_commentaries_by_fixture.ts @@ -0,0 +1,84 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COMMENTARY_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCommentary, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCommentariesByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksGetCommentariesByFixtureResponse extends ToolResponse { + output: { + commentaries: SportmonksCommentary[] + } +} + +export const sportmonksGetCommentariesByFixtureTool: ToolConfig< + SportmonksGetCommentariesByFixtureParams, + SportmonksGetCommentariesByFixtureResponse +> = { + id: 'sportmonks_football_get_commentaries_by_fixture', + name: 'Get Commentaries by Fixture', + description: 'Retrieve textual commentary for a fixture by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;relatedPlayer)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/commentaries/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_commentaries_by_fixture') + } + return { + success: true, + output: { + commentaries: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + commentaries: { + type: 'array', + description: 'Array of commentary entries for the fixture', + items: { type: 'object', properties: SPORTMONKS_COMMENTARY_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_current_leagues_by_team.ts b/apps/sim/tools/sportmonks_football/get_current_leagues_by_team.ts new file mode 100644 index 00000000000..cab3406d2da --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_current_leagues_by_team.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetCurrentLeaguesByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksGetCurrentLeaguesByTeamResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetCurrentLeaguesByTeamTool: ToolConfig< + SportmonksGetCurrentLeaguesByTeamParams, + SportmonksGetCurrentLeaguesByTeamResponse +> = { + id: 'sportmonks_football_get_current_leagues_by_team', + name: 'Get Current Leagues by Team', + description: 'Retrieve all current leagues for a team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/teams/${encodeURIComponent(params.teamId.trim())}/current` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_current_leagues_by_team') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of current league objects for the team', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_expected_lineups_by_player.ts b/apps/sim/tools/sportmonks_football/get_expected_lineups_by_player.ts new file mode 100644 index 00000000000..5257ed1e53e --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_expected_lineups_by_player.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_EXPECTED_LINEUP_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksExpectedLineup, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetExpectedLineupsByPlayerParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + playerId: string +} + +export interface SportmonksGetExpectedLineupsByPlayerResponse extends ToolResponse { + output: { + expectedLineups: SportmonksExpectedLineup[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetExpectedLineupsByPlayerTool: ToolConfig< + SportmonksGetExpectedLineupsByPlayerParams, + SportmonksGetExpectedLineupsByPlayerResponse +> = { + id: 'sportmonks_football_get_expected_lineups_by_player', + name: 'Get Expected Lineups by Player', + description: 'Retrieve the premium expected lineups for a player ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + playerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the player', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. player;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/expected-lineups/players/${encodeURIComponent(params.playerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_expected_lineups_by_player') + } + return { + success: true, + output: { + expectedLineups: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + expectedLineups: { + type: 'array', + description: 'Array of expected lineup entries for the player', + items: { type: 'object', properties: SPORTMONKS_EXPECTED_LINEUP_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_expected_lineups_by_team.ts b/apps/sim/tools/sportmonks_football/get_expected_lineups_by_team.ts new file mode 100644 index 00000000000..40442052990 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_expected_lineups_by_team.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_EXPECTED_LINEUP_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksExpectedLineup, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetExpectedLineupsByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksGetExpectedLineupsByTeamResponse extends ToolResponse { + output: { + expectedLineups: SportmonksExpectedLineup[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetExpectedLineupsByTeamTool: ToolConfig< + SportmonksGetExpectedLineupsByTeamParams, + SportmonksGetExpectedLineupsByTeamResponse +> = { + id: 'sportmonks_football_get_expected_lineups_by_team', + name: 'Get Expected Lineups by Team', + description: 'Retrieve the premium expected lineups for a team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. player;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/expected-lineups/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_expected_lineups_by_team') + } + return { + success: true, + output: { + expectedLineups: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + expectedLineups: { + type: 'array', + description: 'Array of expected lineup entries for the team', + items: { type: 'object', properties: SPORTMONKS_EXPECTED_LINEUP_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_extended_team_squad.ts b/apps/sim/tools/sportmonks_football/get_extended_team_squad.ts new file mode 100644 index 00000000000..20699366a5f --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_extended_team_squad.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SQUAD_PROPERTIES, + type SportmonksSquadEntry, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetExtendedTeamSquadParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksGetExtendedTeamSquadResponse extends ToolResponse { + output: { + squad: SportmonksSquadEntry[] + } +} + +export const sportmonksGetExtendedTeamSquadTool: ToolConfig< + SportmonksGetExtendedTeamSquadParams, + SportmonksGetExtendedTeamSquadResponse +> = { + id: 'sportmonks_football_get_extended_team_squad', + name: 'Get Extended Team Squad', + description: 'Retrieve all squad entries for a team (based on current seasons) by team ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. player;position)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/squads/teams/${encodeURIComponent(params.teamId.trim())}/extended` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_extended_team_squad') + } + return { + success: true, + output: { + squad: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + squad: { + type: 'array', + description: 'Array of extended squad entries for the team', + items: { type: 'object', properties: SPORTMONKS_SQUAD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_fixtures_by_date_range_for_team.ts b/apps/sim/tools/sportmonks_football/get_fixtures_by_date_range_for_team.ts new file mode 100644 index 00000000000..0f998cbc1bf --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_fixtures_by_date_range_for_team.ts @@ -0,0 +1,132 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetFixturesByDateRangeForTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + startDate: string + endDate: string + teamId: string +} + +export interface SportmonksGetFixturesByDateRangeForTeamResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetFixturesByDateRangeForTeamTool: ToolConfig< + SportmonksGetFixturesByDateRangeForTeamParams, + SportmonksGetFixturesByDateRangeForTeamResponse +> = { + id: 'sportmonks_football_get_fixtures_by_date_range_for_team', + name: 'Get Fixtures by Date Range for Team', + description: 'Retrieve fixtures for a team within a date range (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + startDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYY-MM-DD format', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/between/${encodeURIComponent( + params.startDate.trim() + )}/${encodeURIComponent(params.endDate.trim())}/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_date_range_for_team') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of fixture objects for the team within the date range', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_fixtures_by_ids.ts b/apps/sim/tools/sportmonks_football/get_fixtures_by_ids.ts new file mode 100644 index 00000000000..3e5b6a2a3e9 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_fixtures_by_ids.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetFixturesByIdsParams extends SportmonksBaseParams { + ids: string +} + +export interface SportmonksGetFixturesByIdsResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + } +} + +export const sportmonksGetFixturesByIdsTool: ToolConfig< + SportmonksGetFixturesByIdsParams, + SportmonksGetFixturesByIdsResponse +> = { + id: 'sportmonks_football_get_fixtures_by_ids', + name: 'Get Fixtures by Multiple IDs', + description: 'Retrieve multiple football fixtures by a comma-separated list of IDs (max 50)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + ids: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated fixture IDs (e.g. 18535517,18535518). Maximum of 50 IDs', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/multi/${encodeURIComponent(params.ids.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_ids') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of fixture objects for the requested IDs', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_grouped_standings_by_round.ts b/apps/sim/tools/sportmonks_football/get_grouped_standings_by_round.ts new file mode 100644 index 00000000000..9fd418cf50e --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_grouped_standings_by_round.ts @@ -0,0 +1,87 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_FOOTBALL_BASE_URL } from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetGroupedStandingsByRoundParams extends SportmonksBaseParams { + roundId: string +} + +export interface SportmonksGetGroupedStandingsByRoundResponse extends ToolResponse { + output: { + standings: unknown[] + } +} + +export const sportmonksGetGroupedStandingsByRoundTool: ToolConfig< + SportmonksGetGroupedStandingsByRoundParams, + SportmonksGetGroupedStandingsByRoundResponse +> = { + id: 'sportmonks_football_get_grouped_standings_by_round', + name: 'Get Grouped Standings by Round', + description: + 'Retrieve the standing table for a round ID grouped by group where applicable from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + roundId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the round', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. standingGroups:246697)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/standings/rounds/${encodeURIComponent(params.roundId.trim())}/grouped` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_grouped_standings_by_round') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + standings: { + type: 'json', + description: + 'Standings for the round: an array of groups (each with id, name and a standings array) when groups exist, otherwise a flat array of standing entries', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_latest_coaches.ts b/apps/sim/tools/sportmonks_football/get_latest_coaches.ts new file mode 100644 index 00000000000..838950deda7 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_latest_coaches.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COACH_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCoach, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLatestCoachesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetLatestCoachesResponse extends ToolResponse { + output: { + coaches: SportmonksCoach[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLatestCoachesTool: ToolConfig< + SportmonksGetLatestCoachesParams, + SportmonksGetLatestCoachesResponse +> = { + id: 'sportmonks_football_get_latest_coaches', + name: 'Get Last Updated Coaches', + description: 'Retrieve all coaches that have received updates in the past two hours', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;nationality)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/coaches/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_coaches') + } + return { + success: true, + output: { + coaches: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + coaches: { + type: 'array', + description: 'Array of recently updated coach objects', + items: { type: 'object', properties: SPORTMONKS_COACH_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_latest_fixtures.ts b/apps/sim/tools/sportmonks_football/get_latest_fixtures.ts new file mode 100644 index 00000000000..61054ab1ce4 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_latest_fixtures.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLatestFixturesParams extends SportmonksBaseParams {} + +export interface SportmonksGetLatestFixturesResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + } +} + +export const sportmonksGetLatestFixturesTool: ToolConfig< + SportmonksGetLatestFixturesParams, + SportmonksGetLatestFixturesResponse +> = { + id: 'sportmonks_football_get_latest_fixtures', + name: 'Get Latest Updated Fixtures', + description: 'Retrieve all fixtures that have received updates within the last 10 seconds', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_fixtures') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of recently updated fixture objects', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_latest_livescores.ts b/apps/sim/tools/sportmonks_football/get_latest_livescores.ts new file mode 100644 index 00000000000..8034d3cdc9a --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_latest_livescores.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLatestLivescoresParams extends SportmonksBaseParams {} + +export interface SportmonksGetLatestLivescoresResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + } +} + +export const sportmonksGetLatestLivescoresTool: ToolConfig< + SportmonksGetLatestLivescoresParams, + SportmonksGetLatestLivescoresResponse +> = { + id: 'sportmonks_football_get_latest_livescores', + name: 'Get Latest Updated Livescores', + description: 'Retrieve all livescores that have received updates within the last 10 seconds', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/livescores/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_livescores') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of recently updated live fixture objects', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_latest_players.ts b/apps/sim/tools/sportmonks_football/get_latest_players.ts new file mode 100644 index 00000000000..8e7b17c9ad6 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_latest_players.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PLAYER_PROPERTIES, + type SportmonksPlayer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLatestPlayersParams extends SportmonksBaseParams {} + +export interface SportmonksGetLatestPlayersResponse extends ToolResponse { + output: { + players: SportmonksPlayer[] + } +} + +export const sportmonksGetLatestPlayersTool: ToolConfig< + SportmonksGetLatestPlayersParams, + SportmonksGetLatestPlayersResponse +> = { + id: 'sportmonks_football_get_latest_players', + name: 'Get Last Updated Players', + description: 'Retrieve all players that have received updates in the past two hours', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. nationality;position)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/players/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_players') + } + return { + success: true, + output: { + players: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + players: { + type: 'array', + description: 'Array of recently updated player objects', + items: { type: 'object', properties: SPORTMONKS_PLAYER_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_latest_totw.ts b/apps/sim/tools/sportmonks_football/get_latest_totw.ts new file mode 100644 index 00000000000..41ba7cad8ab --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_latest_totw.ts @@ -0,0 +1,84 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TOTW_PROPERTIES, + type SportmonksTotw, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLatestTotwParams extends SportmonksBaseParams { + leagueId: string +} + +export interface SportmonksGetLatestTotwResponse extends ToolResponse { + output: { + totw: SportmonksTotw[] + } +} + +export const sportmonksGetLatestTotwTool: ToolConfig< + SportmonksGetLatestTotwParams, + SportmonksGetLatestTotwResponse +> = { + id: 'sportmonks_football_get_latest_totw', + name: 'Get Latest Team of the Week', + description: 'Retrieve the latest Team of the Week (TOTW) for a league ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + leagueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the league', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. fixture;team;player;round)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/team-of-the-week/leagues/${encodeURIComponent(params.leagueId.trim())}/latest` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_totw') + } + return { + success: true, + output: { + totw: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + totw: { + type: 'array', + description: 'Array of the latest Team of the Week entries for the league', + items: { type: 'object', properties: SPORTMONKS_TOTW_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_latest_transfers.ts b/apps/sim/tools/sportmonks_football/get_latest_transfers.ts new file mode 100644 index 00000000000..7137712c4f4 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_latest_transfers.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_PROPERTIES, + type SportmonksTransfer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLatestTransfersParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetLatestTransfersResponse extends ToolResponse { + output: { + transfers: SportmonksTransfer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLatestTransfersTool: ToolConfig< + SportmonksGetLatestTransfersParams, + SportmonksGetLatestTransfersResponse +> = { + id: 'sportmonks_football_get_latest_transfers', + name: 'Get Latest Transfers', + description: 'Retrieve the latest transfers available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. transferTypes:219,220)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/transfers/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_transfers') + } + return { + success: true, + output: { + transfers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transfers: { + type: 'array', + description: 'Array of the latest transfer objects', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_leagues_by_country.ts b/apps/sim/tools/sportmonks_football/get_leagues_by_country.ts new file mode 100644 index 00000000000..03a1caa1e07 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_leagues_by_country.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLeaguesByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksGetLeaguesByCountryResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLeaguesByCountryTool: ToolConfig< + SportmonksGetLeaguesByCountryParams, + SportmonksGetLeaguesByCountryResponse +> = { + id: 'sportmonks_football_get_leagues_by_country', + name: 'Get Leagues by Country', + description: 'Retrieve all leagues for a country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_country') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects for the country', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_leagues_by_date.ts b/apps/sim/tools/sportmonks_football/get_leagues_by_date.ts new file mode 100644 index 00000000000..db2c44aaa54 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_leagues_by_date.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLeaguesByDateParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + date: string +} + +export interface SportmonksGetLeaguesByDateResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLeaguesByDateTool: ToolConfig< + SportmonksGetLeaguesByDateParams, + SportmonksGetLeaguesByDateResponse +> = { + id: 'sportmonks_football_get_leagues_by_date', + name: 'Get Leagues by Date', + description: 'Retrieve all leagues with fixtures on a given date (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + date: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The fixture date in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/date/${encodeURIComponent(params.date.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_date') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects with fixtures on the requested date', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_leagues_by_team.ts b/apps/sim/tools/sportmonks_football/get_leagues_by_team.ts new file mode 100644 index 00000000000..342494e43d1 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_leagues_by_team.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLeaguesByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksGetLeaguesByTeamResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLeaguesByTeamTool: ToolConfig< + SportmonksGetLeaguesByTeamParams, + SportmonksGetLeaguesByTeamResponse +> = { + id: 'sportmonks_football_get_leagues_by_team', + name: 'Get Leagues by Team', + description: 'Retrieve all current and historical leagues for a team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_team') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of current and historical league objects for the team', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_live_leagues.ts b/apps/sim/tools/sportmonks_football/get_live_leagues.ts new file mode 100644 index 00000000000..3c2a22c2c5e --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_live_leagues.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLiveLeaguesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetLiveLeaguesResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLiveLeaguesTool: ToolConfig< + SportmonksGetLiveLeaguesParams, + SportmonksGetLiveLeaguesResponse +> = { + id: 'sportmonks_football_get_live_leagues', + name: 'Get Live Leagues', + description: 'Retrieve all leagues that have fixtures currently being played from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/live`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_live_leagues') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of currently live league objects', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_live_probabilities.ts b/apps/sim/tools/sportmonks_football/get_live_probabilities.ts new file mode 100644 index 00000000000..51c8f3dcfe4 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_live_probabilities.ts @@ -0,0 +1,102 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LIVE_PROBABILITY_PROPERTIES, + type SportmonksLiveProbability, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLiveProbabilitiesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetLiveProbabilitiesResponse extends ToolResponse { + output: { + predictions: SportmonksLiveProbability[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLiveProbabilitiesTool: ToolConfig< + SportmonksGetLiveProbabilitiesParams, + SportmonksGetLiveProbabilitiesResponse +> = { + id: 'sportmonks_football_get_live_probabilities', + name: 'Get Live Probabilities', + description: 'Retrieve all live (in-play) prediction probabilities from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;fixture)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery( + `${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/live/probabilities`, + params + ), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_live_probabilities') + } + return { + success: true, + output: { + predictions: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + predictions: { + type: 'array', + description: 'Array of live probability prediction objects', + items: { type: 'object', properties: SPORTMONKS_LIVE_PROBABILITY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_live_probabilities_by_fixture.ts b/apps/sim/tools/sportmonks_football/get_live_probabilities_by_fixture.ts new file mode 100644 index 00000000000..b60e5370e8c --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_live_probabilities_by_fixture.ts @@ -0,0 +1,110 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LIVE_PROBABILITY_PROPERTIES, + type SportmonksLiveProbability, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLiveProbabilitiesByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetLiveProbabilitiesByFixtureResponse extends ToolResponse { + output: { + predictions: SportmonksLiveProbability[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetLiveProbabilitiesByFixtureTool: ToolConfig< + SportmonksGetLiveProbabilitiesByFixtureParams, + SportmonksGetLiveProbabilitiesByFixtureResponse +> = { + id: 'sportmonks_football_get_live_probabilities_by_fixture', + name: 'Get Live Probabilities by Fixture', + description: + 'Retrieve all live (in-play) prediction probabilities for a fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;fixture)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/live/probabilities/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_live_probabilities_by_fixture') + } + return { + success: true, + output: { + predictions: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + predictions: { + type: 'array', + description: 'Array of live probability prediction objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_LIVE_PROBABILITY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_live_standings_by_league.ts b/apps/sim/tools/sportmonks_football/get_live_standings_by_league.ts new file mode 100644 index 00000000000..7635098992b --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_live_standings_by_league.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STANDING_PROPERTIES, + type SportmonksStanding, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLiveStandingsByLeagueParams extends SportmonksBaseParams { + leagueId: string +} + +export interface SportmonksGetLiveStandingsByLeagueResponse extends ToolResponse { + output: { + standings: SportmonksStanding[] + } +} + +export const sportmonksGetLiveStandingsByLeagueTool: ToolConfig< + SportmonksGetLiveStandingsByLeagueParams, + SportmonksGetLiveStandingsByLeagueResponse +> = { + id: 'sportmonks_football_get_live_standings_by_league', + name: 'Get Live Standings by League', + description: 'Retrieve the live standing table for a league ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + leagueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the league', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. standingGroups:246697)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/standings/live/leagues/${encodeURIComponent(params.leagueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_live_standings_by_league') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of live standing entries for the league', + items: { type: 'object', properties: SPORTMONKS_STANDING_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_match_facts.ts b/apps/sim/tools/sportmonks_football/get_match_facts.ts new file mode 100644 index 00000000000..12509e39f51 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_match_facts.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_MATCH_FACT_PROPERTIES, + type SportmonksMatchFact, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMatchFactsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetMatchFactsResponse extends ToolResponse { + output: { + matchFacts: SportmonksMatchFact[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetMatchFactsTool: ToolConfig< + SportmonksGetMatchFactsParams, + SportmonksGetMatchFactsResponse +> = { + id: 'sportmonks_football_get_match_facts', + name: 'Get All Match Facts', + description: 'Retrieve all available match facts within your Sportmonks subscription (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;sport;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. matchFactTypes:76088)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/match-facts`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_match_facts') + } + return { + success: true, + output: { + matchFacts: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + matchFacts: { + type: 'array', + description: 'Array of match fact objects', + items: { type: 'object', properties: SPORTMONKS_MATCH_FACT_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_match_facts_by_date_range.ts b/apps/sim/tools/sportmonks_football/get_match_facts_by_date_range.ts new file mode 100644 index 00000000000..271815d1aa9 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_match_facts_by_date_range.ts @@ -0,0 +1,124 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_MATCH_FACT_PROPERTIES, + type SportmonksMatchFact, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMatchFactsByDateRangeParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + startDate: string + endDate: string +} + +export interface SportmonksGetMatchFactsByDateRangeResponse extends ToolResponse { + output: { + matchFacts: SportmonksMatchFact[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetMatchFactsByDateRangeTool: ToolConfig< + SportmonksGetMatchFactsByDateRangeParams, + SportmonksGetMatchFactsByDateRangeResponse +> = { + id: 'sportmonks_football_get_match_facts_by_date_range', + name: 'Get Match Facts by Date Range', + description: 'Retrieve match facts within a date range (YYYY-MM-DD) from Sportmonks (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + startDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;sport;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. matchFactTypes:76088)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/match-facts/fixtures/between/${encodeURIComponent( + params.startDate.trim() + )}/${encodeURIComponent(params.endDate.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_match_facts_by_date_range') + } + return { + success: true, + output: { + matchFacts: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + matchFacts: { + type: 'array', + description: 'Array of match fact objects within the date range', + items: { type: 'object', properties: SPORTMONKS_MATCH_FACT_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_match_facts_by_fixture.ts b/apps/sim/tools/sportmonks_football/get_match_facts_by_fixture.ts new file mode 100644 index 00000000000..1faa2d5b864 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_match_facts_by_fixture.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_MATCH_FACT_PROPERTIES, + type SportmonksMatchFact, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMatchFactsByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetMatchFactsByFixtureResponse extends ToolResponse { + output: { + matchFacts: SportmonksMatchFact[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetMatchFactsByFixtureTool: ToolConfig< + SportmonksGetMatchFactsByFixtureParams, + SportmonksGetMatchFactsByFixtureResponse +> = { + id: 'sportmonks_football_get_match_facts_by_fixture', + name: 'Get Match Facts by Fixture', + description: 'Retrieve match facts for a fixture ID from Sportmonks (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;sport;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. matchFactTypes:76088)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/match-facts/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_match_facts_by_fixture') + } + return { + success: true, + output: { + matchFacts: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + matchFacts: { + type: 'array', + description: 'Array of match fact objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MATCH_FACT_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_match_facts_by_league.ts b/apps/sim/tools/sportmonks_football/get_match_facts_by_league.ts new file mode 100644 index 00000000000..ba75c97ac3b --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_match_facts_by_league.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_MATCH_FACT_PROPERTIES, + type SportmonksMatchFact, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetMatchFactsByLeagueParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + leagueId: string +} + +export interface SportmonksGetMatchFactsByLeagueResponse extends ToolResponse { + output: { + matchFacts: SportmonksMatchFact[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetMatchFactsByLeagueTool: ToolConfig< + SportmonksGetMatchFactsByLeagueParams, + SportmonksGetMatchFactsByLeagueResponse +> = { + id: 'sportmonks_football_get_match_facts_by_league', + name: 'Get Match Facts by League', + description: 'Retrieve match facts for a league ID from Sportmonks (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + leagueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the league', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;sport;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. matchFactTypes:76088)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/match-facts/leagues/${encodeURIComponent(params.leagueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_match_facts_by_league') + } + return { + success: true, + output: { + matchFacts: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + matchFacts: { + type: 'array', + description: 'Array of match fact objects for the league', + items: { type: 'object', properties: SPORTMONKS_MATCH_FACT_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_past_fixtures_by_tv_station.ts b/apps/sim/tools/sportmonks_football/get_past_fixtures_by_tv_station.ts new file mode 100644 index 00000000000..d66542942a8 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_past_fixtures_by_tv_station.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPastFixturesByTvStationParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + tvStationId: string +} + +export interface SportmonksGetPastFixturesByTvStationResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPastFixturesByTvStationTool: ToolConfig< + SportmonksGetPastFixturesByTvStationParams, + SportmonksGetPastFixturesByTvStationResponse +> = { + id: 'sportmonks_football_get_past_fixtures_by_tv_station', + name: 'Get Past Fixtures by TV Station', + description: 'Retrieve all past fixtures that were available for a TV station ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + tvStationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the TV station', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participants)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/past/tv-stations/${encodeURIComponent(params.tvStationId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_past_fixtures_by_tv_station') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of past fixture objects for the TV station', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_players_by_country.ts b/apps/sim/tools/sportmonks_football/get_players_by_country.ts new file mode 100644 index 00000000000..54c6e4120c6 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_players_by_country.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PLAYER_PROPERTIES, + type SportmonksPlayer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPlayersByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksGetPlayersByCountryResponse extends ToolResponse { + output: { + players: SportmonksPlayer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPlayersByCountryTool: ToolConfig< + SportmonksGetPlayersByCountryParams, + SportmonksGetPlayersByCountryResponse +> = { + id: 'sportmonks_football_get_players_by_country', + name: 'Get Players by Country', + description: 'Retrieve all players for a country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. nationality;position)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order players by id (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/players/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_players_by_country') + } + return { + success: true, + output: { + players: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + players: { + type: 'array', + description: 'Array of player objects for the country', + items: { type: 'object', properties: SPORTMONKS_PLAYER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_postmatch_news.ts b/apps/sim/tools/sportmonks_football/get_postmatch_news.ts new file mode 100644 index 00000000000..c5f97bd9747 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_postmatch_news.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_NEWS_PROPERTIES, + type SportmonksNews, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPostmatchNewsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetPostmatchNewsResponse extends ToolResponse { + output: { + news: SportmonksNews[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPostmatchNewsTool: ToolConfig< + SportmonksGetPostmatchNewsParams, + SportmonksGetPostmatchNewsResponse +> = { + id: 'sportmonks_football_get_postmatch_news', + name: 'Get Post-Match News', + description: + 'Retrieve all post-match news articles available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixture;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. newsitemLeagues:8)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order news by id (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/news/post-match`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_postmatch_news') + } + return { + success: true, + output: { + news: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + news: { + type: 'array', + description: 'Array of post-match news articles', + items: { type: 'object', properties: SPORTMONKS_NEWS_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_postmatch_news_by_season.ts b/apps/sim/tools/sportmonks_football/get_postmatch_news_by_season.ts new file mode 100644 index 00000000000..ac85500fb49 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_postmatch_news_by_season.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_NEWS_PROPERTIES, + type SportmonksNews, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPostmatchNewsBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksGetPostmatchNewsBySeasonResponse extends ToolResponse { + output: { + news: SportmonksNews[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPostmatchNewsBySeasonTool: ToolConfig< + SportmonksGetPostmatchNewsBySeasonParams, + SportmonksGetPostmatchNewsBySeasonResponse +> = { + id: 'sportmonks_football_get_postmatch_news_by_season', + name: 'Get Post-Match News by Season', + description: 'Retrieve all post-match news articles for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixture;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. newsitemLeagues:8)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order news (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/news/post-match/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_postmatch_news_by_season') + } + return { + success: true, + output: { + news: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + news: { + type: 'array', + description: 'Array of post-match news articles for the season', + items: { type: 'object', properties: SPORTMONKS_NEWS_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_predictability_by_league.ts b/apps/sim/tools/sportmonks_football/get_predictability_by_league.ts new file mode 100644 index 00000000000..fc3fe92497f --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_predictability_by_league.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PREDICTABILITY_PROPERTIES, + type SportmonksPredictability, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPredictabilityByLeagueParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + leagueId: string +} + +export interface SportmonksGetPredictabilityByLeagueResponse extends ToolResponse { + output: { + predictability: SportmonksPredictability[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPredictabilityByLeagueTool: ToolConfig< + SportmonksGetPredictabilityByLeagueParams, + SportmonksGetPredictabilityByLeagueResponse +> = { + id: 'sportmonks_football_get_predictability_by_league', + name: 'Get Predictability by League', + description: 'Retrieve the predictions model performance for a league ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + leagueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the league', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. predictabilityTypes:245)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/predictability/leagues/${encodeURIComponent(params.leagueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_predictability_by_league') + } + return { + success: true, + output: { + predictability: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + predictability: { + type: 'array', + description: 'Array of predictability records for the league', + items: { type: 'object', properties: SPORTMONKS_PREDICTABILITY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_prematch_news.ts b/apps/sim/tools/sportmonks_football/get_prematch_news.ts new file mode 100644 index 00000000000..e2676de2ba5 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_prematch_news.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_NEWS_PROPERTIES, + type SportmonksNews, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPrematchNewsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetPrematchNewsResponse extends ToolResponse { + output: { + news: SportmonksNews[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPrematchNewsTool: ToolConfig< + SportmonksGetPrematchNewsParams, + SportmonksGetPrematchNewsResponse +> = { + id: 'sportmonks_football_get_prematch_news', + name: 'Get Pre-Match News', + description: 'Retrieve all pre-match news articles available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixture;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. newsitemLeagues:8)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order news by id (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/news/pre-match`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_prematch_news') + } + return { + success: true, + output: { + news: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + news: { + type: 'array', + description: 'Array of pre-match news articles', + items: { type: 'object', properties: SPORTMONKS_NEWS_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_prematch_news_by_season.ts b/apps/sim/tools/sportmonks_football/get_prematch_news_by_season.ts new file mode 100644 index 00000000000..378981bac84 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_prematch_news_by_season.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_NEWS_PROPERTIES, + type SportmonksNews, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPrematchNewsBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksGetPrematchNewsBySeasonResponse extends ToolResponse { + output: { + news: SportmonksNews[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPrematchNewsBySeasonTool: ToolConfig< + SportmonksGetPrematchNewsBySeasonParams, + SportmonksGetPrematchNewsBySeasonResponse +> = { + id: 'sportmonks_football_get_prematch_news_by_season', + name: 'Get Pre-Match News by Season', + description: 'Retrieve all pre-match news articles for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixture;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. newsitemLeagues:8)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order news (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/news/pre-match/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_prematch_news_by_season') + } + return { + success: true, + output: { + news: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + news: { + type: 'array', + description: 'Array of pre-match news articles for the season', + items: { type: 'object', properties: SPORTMONKS_NEWS_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_prematch_news_upcoming.ts b/apps/sim/tools/sportmonks_football/get_prematch_news_upcoming.ts new file mode 100644 index 00000000000..06e8b22bd7c --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_prematch_news_upcoming.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_NEWS_PROPERTIES, + type SportmonksNews, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPrematchNewsUpcomingParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetPrematchNewsUpcomingResponse extends ToolResponse { + output: { + news: SportmonksNews[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetPrematchNewsUpcomingTool: ToolConfig< + SportmonksGetPrematchNewsUpcomingParams, + SportmonksGetPrematchNewsUpcomingResponse +> = { + id: 'sportmonks_football_get_prematch_news_upcoming', + name: 'Get Pre-Match News for Upcoming Fixtures', + description: 'Retrieve all pre-match news articles for upcoming fixtures from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixture;league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. newsitemLeagues:8)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order news (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/news/pre-match/upcoming`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_prematch_news_upcoming') + } + return { + success: true, + output: { + news: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + news: { + type: 'array', + description: 'Array of pre-match news articles for upcoming fixtures', + items: { type: 'object', properties: SPORTMONKS_NEWS_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_probabilities.ts b/apps/sim/tools/sportmonks_football/get_probabilities.ts new file mode 100644 index 00000000000..088cf6fa764 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_probabilities.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PREDICTION_PROPERTIES, + type SportmonksPrediction, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetProbabilitiesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetProbabilitiesResponse extends ToolResponse { + output: { + predictions: SportmonksPrediction[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetProbabilitiesTool: ToolConfig< + SportmonksGetProbabilitiesParams, + SportmonksGetProbabilitiesResponse +> = { + id: 'sportmonks_football_get_probabilities', + name: 'Get Probabilities', + description: + 'Retrieve all prediction probabilities available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. predictionTypes:236)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/probabilities`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_probabilities') + } + return { + success: true, + output: { + predictions: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + predictions: { + type: 'array', + description: 'Array of prediction probability objects', + items: { type: 'object', properties: SPORTMONKS_PREDICTION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_probabilities_by_fixture.ts b/apps/sim/tools/sportmonks_football/get_probabilities_by_fixture.ts new file mode 100644 index 00000000000..c8fcf16235f --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_probabilities_by_fixture.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PREDICTION_PROPERTIES, + type SportmonksPrediction, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetProbabilitiesByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetProbabilitiesByFixtureResponse extends ToolResponse { + output: { + predictions: SportmonksPrediction[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetProbabilitiesByFixtureTool: ToolConfig< + SportmonksGetProbabilitiesByFixtureParams, + SportmonksGetProbabilitiesByFixtureResponse +> = { + id: 'sportmonks_football_get_probabilities_by_fixture', + name: 'Get Predictions by Fixture', + description: 'Retrieve prediction probabilities for a fixture by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;fixture)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. predictionTypes:236)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/probabilities/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_probabilities_by_fixture') + } + return { + success: true, + output: { + predictions: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + predictions: { + type: 'array', + description: 'Array of prediction probability entries for the fixture', + items: { type: 'object', properties: SPORTMONKS_PREDICTION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_referee.ts b/apps/sim/tools/sportmonks_football/get_referee.ts new file mode 100644 index 00000000000..af77a486fbf --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_referee.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_REFEREE_PROPERTIES, + type SportmonksReferee, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRefereeParams extends SportmonksBaseParams { + refereeId: string +} + +export interface SportmonksGetRefereeResponse extends ToolResponse { + output: { + referee: SportmonksReferee | null + } +} + +export const sportmonksGetRefereeTool: ToolConfig< + SportmonksGetRefereeParams, + SportmonksGetRefereeResponse +> = { + id: 'sportmonks_football_get_referee', + name: 'Get Referee by ID', + description: 'Retrieve a single football referee by their ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + refereeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the referee', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;statistics)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/referees/${encodeURIComponent(params.refereeId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_referee') + } + return { + success: true, + output: { + referee: data.data ?? null, + }, + } + }, + + outputs: { + referee: { + type: 'object', + description: 'The requested referee object', + properties: SPORTMONKS_REFEREE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_referees.ts b/apps/sim/tools/sportmonks_football/get_referees.ts new file mode 100644 index 00000000000..e2d6d630c31 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_referees.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_REFEREE_PROPERTIES, + type SportmonksReferee, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRefereesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetRefereesResponse extends ToolResponse { + output: { + referees: SportmonksReferee[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetRefereesTool: ToolConfig< + SportmonksGetRefereesParams, + SportmonksGetRefereesResponse +> = { + id: 'sportmonks_football_get_referees', + name: 'Get Referees', + description: 'Retrieve all football referees available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;statistics)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. refereeCountries:44)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/referees`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_referees') + } + return { + success: true, + output: { + referees: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + referees: { + type: 'array', + description: 'Array of referee objects', + items: { type: 'object', properties: SPORTMONKS_REFEREE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_referees_by_country.ts b/apps/sim/tools/sportmonks_football/get_referees_by_country.ts new file mode 100644 index 00000000000..c69c5028586 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_referees_by_country.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_REFEREE_PROPERTIES, + type SportmonksReferee, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRefereesByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksGetRefereesByCountryResponse extends ToolResponse { + output: { + referees: SportmonksReferee[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetRefereesByCountryTool: ToolConfig< + SportmonksGetRefereesByCountryParams, + SportmonksGetRefereesByCountryResponse +> = { + id: 'sportmonks_football_get_referees_by_country', + name: 'Get Referees by Country', + description: 'Retrieve all referees for a country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;nationality)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/referees/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_referees_by_country') + } + return { + success: true, + output: { + referees: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + referees: { + type: 'array', + description: 'Array of referee objects for the country', + items: { type: 'object', properties: SPORTMONKS_REFEREE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_referees_by_season.ts b/apps/sim/tools/sportmonks_football/get_referees_by_season.ts new file mode 100644 index 00000000000..eaa8bb91790 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_referees_by_season.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_REFEREE_PROPERTIES, + type SportmonksReferee, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRefereesBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksGetRefereesBySeasonResponse extends ToolResponse { + output: { + referees: SportmonksReferee[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetRefereesBySeasonTool: ToolConfig< + SportmonksGetRefereesBySeasonParams, + SportmonksGetRefereesBySeasonResponse +> = { + id: 'sportmonks_football_get_referees_by_season', + name: 'Get Referees by Season', + description: 'Retrieve all referees for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;nationality)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/referees/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_referees_by_season') + } + return { + success: true, + output: { + referees: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + referees: { + type: 'array', + description: 'Array of referee objects for the season', + items: { type: 'object', properties: SPORTMONKS_REFEREE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_rivals_by_team.ts b/apps/sim/tools/sportmonks_football/get_rivals_by_team.ts new file mode 100644 index 00000000000..bdfff82d89d --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_rivals_by_team.ts @@ -0,0 +1,83 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_RIVAL_PROPERTIES, + type SportmonksRival, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRivalsByTeamParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksGetRivalsByTeamResponse extends ToolResponse { + output: { + rivals: SportmonksRival[] + } +} + +export const sportmonksGetRivalsByTeamTool: ToolConfig< + SportmonksGetRivalsByTeamParams, + SportmonksGetRivalsByTeamResponse +> = { + id: 'sportmonks_football_get_rivals_by_team', + name: 'Get Rivals by Team', + description: 'Retrieve rival teams for a team by team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. team;rival)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/rivals/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_rivals_by_team') + } + return { + success: true, + output: { + rivals: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + rivals: { + type: 'array', + description: 'Array of rival relationships for the team', + items: { type: 'object', properties: SPORTMONKS_RIVAL_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_round.ts b/apps/sim/tools/sportmonks_football/get_round.ts new file mode 100644 index 00000000000..df469177561 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_round.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_ROUND_PROPERTIES, + type SportmonksRound, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRoundParams extends SportmonksBaseParams { + roundId: string +} + +export interface SportmonksGetRoundResponse extends ToolResponse { + output: { + round: SportmonksRound | null + } +} + +export const sportmonksGetRoundTool: ToolConfig< + SportmonksGetRoundParams, + SportmonksGetRoundResponse +> = { + id: 'sportmonks_football_get_round', + name: 'Get Round by ID', + description: 'Retrieve a single football round by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + roundId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the round', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;stage;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/rounds/${encodeURIComponent(params.roundId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_round') + } + return { + success: true, + output: { + round: data.data ?? null, + }, + } + }, + + outputs: { + round: { + type: 'object', + description: 'The requested round object', + properties: SPORTMONKS_ROUND_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_round_statistics.ts b/apps/sim/tools/sportmonks_football/get_round_statistics.ts new file mode 100644 index 00000000000..119024e3a6b --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_round_statistics.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STATISTIC_PROPERTIES, + type SportmonksStatistic, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRoundStatisticsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + roundId: string +} + +export interface SportmonksGetRoundStatisticsResponse extends ToolResponse { + output: { + statistics: SportmonksStatistic[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetRoundStatisticsTool: ToolConfig< + SportmonksGetRoundStatisticsParams, + SportmonksGetRoundStatisticsResponse +> = { + id: 'sportmonks_football_get_round_statistics', + name: 'Get Round Statistics', + description: 'Retrieve all available statistics for a round ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + roundId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the round', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. seasonstatisticTypes:52,88)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/statistics/rounds/${encodeURIComponent(params.roundId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_round_statistics') + } + return { + success: true, + output: { + statistics: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + statistics: { + type: 'array', + description: 'Array of statistic entries for the round', + items: { type: 'object', properties: SPORTMONKS_STATISTIC_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_rounds.ts b/apps/sim/tools/sportmonks_football/get_rounds.ts new file mode 100644 index 00000000000..2503e34f0d9 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_rounds.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_ROUND_PROPERTIES, + type SportmonksRound, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRoundsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetRoundsResponse extends ToolResponse { + output: { + rounds: SportmonksRound[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetRoundsTool: ToolConfig< + SportmonksGetRoundsParams, + SportmonksGetRoundsResponse +> = { + id: 'sportmonks_football_get_rounds', + name: 'Get Rounds', + description: 'Retrieve all football rounds available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;stage)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. roundSeasons:19735)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/rounds`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_rounds') + } + return { + success: true, + output: { + rounds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + rounds: { + type: 'array', + description: 'Array of round objects', + items: { type: 'object', properties: SPORTMONKS_ROUND_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_rounds_by_season.ts b/apps/sim/tools/sportmonks_football/get_rounds_by_season.ts new file mode 100644 index 00000000000..47c9dbda155 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_rounds_by_season.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_ROUND_PROPERTIES, + type SportmonksRound, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetRoundsBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetRoundsBySeasonResponse extends ToolResponse { + output: { + rounds: SportmonksRound[] + } +} + +export const sportmonksGetRoundsBySeasonTool: ToolConfig< + SportmonksGetRoundsBySeasonParams, + SportmonksGetRoundsBySeasonResponse +> = { + id: 'sportmonks_football_get_rounds_by_season', + name: 'Get Rounds by Season', + description: 'Retrieve all rounds for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;stage)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/rounds/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_rounds_by_season') + } + return { + success: true, + output: { + rounds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + rounds: { + type: 'array', + description: 'Array of round objects for the season', + items: { type: 'object', properties: SPORTMONKS_ROUND_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_schedules_by_season.ts b/apps/sim/tools/sportmonks_football/get_schedules_by_season.ts new file mode 100644 index 00000000000..c2a3a6fcfe3 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_schedules_by_season.ts @@ -0,0 +1,73 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_FOOTBALL_BASE_URL } from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetSchedulesBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetSchedulesBySeasonResponse extends ToolResponse { + output: { + schedules: unknown[] + } +} + +export const sportmonksGetSchedulesBySeasonTool: ToolConfig< + SportmonksGetSchedulesBySeasonParams, + SportmonksGetSchedulesBySeasonResponse +> = { + id: 'sportmonks_football_get_schedules_by_season', + name: 'Get Schedules by Season', + description: 'Retrieve the full schedule (stages, rounds and fixtures) for a season by season ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/schedules/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_schedules_by_season') + } + return { + success: true, + output: { + schedules: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + schedules: { + type: 'json', + description: + 'Array of stages, each with nested rounds and their fixtures (participants, scores)', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_schedules_by_season_and_team.ts b/apps/sim/tools/sportmonks_football/get_schedules_by_season_and_team.ts new file mode 100644 index 00000000000..63b55349c17 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_schedules_by_season_and_team.ts @@ -0,0 +1,82 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_FOOTBALL_BASE_URL } from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetSchedulesBySeasonAndTeamParams extends SportmonksBaseParams { + seasonId: string + teamId: string +} + +export interface SportmonksGetSchedulesBySeasonAndTeamResponse extends ToolResponse { + output: { + schedules: unknown[] + } +} + +export const sportmonksGetSchedulesBySeasonAndTeamTool: ToolConfig< + SportmonksGetSchedulesBySeasonAndTeamParams, + SportmonksGetSchedulesBySeasonAndTeamResponse +> = { + id: 'sportmonks_football_get_schedules_by_season_and_team', + name: 'Get Schedules by Season and Team', + description: 'Retrieve the full season schedule for a specific team by season ID and team ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/schedules/seasons/${encodeURIComponent( + params.seasonId.trim() + )}/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_schedules_by_season_and_team') + } + return { + success: true, + output: { + schedules: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + schedules: { + type: 'json', + description: + 'Array of stages, each with nested rounds and their fixtures for the team in the season', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_schedules_by_team.ts b/apps/sim/tools/sportmonks_football/get_schedules_by_team.ts new file mode 100644 index 00000000000..2c2986a5f34 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_schedules_by_team.ts @@ -0,0 +1,73 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { SPORTMONKS_FOOTBALL_BASE_URL } from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetSchedulesByTeamParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksGetSchedulesByTeamResponse extends ToolResponse { + output: { + schedules: unknown[] + } +} + +export const sportmonksGetSchedulesByTeamTool: ToolConfig< + SportmonksGetSchedulesByTeamParams, + SportmonksGetSchedulesByTeamResponse +> = { + id: 'sportmonks_football_get_schedules_by_team', + name: 'Get Schedules by Team', + description: 'Retrieve the full schedule (stages, rounds and fixtures) for a team by team ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/schedules/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_schedules_by_team') + } + return { + success: true, + output: { + schedules: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + schedules: { + type: 'json', + description: + 'Array of stages, each with nested rounds and their fixtures (participants, scores)', + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_season.ts b/apps/sim/tools/sportmonks_football/get_season.ts new file mode 100644 index 00000000000..327c4b60a49 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_season.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SEASON_PROPERTIES, + type SportmonksSeason, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetSeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetSeasonResponse extends ToolResponse { + output: { + season: SportmonksSeason | null + } +} + +export const sportmonksGetSeasonTool: ToolConfig< + SportmonksGetSeasonParams, + SportmonksGetSeasonResponse +> = { + id: 'sportmonks_football_get_season', + name: 'Get Season by ID', + description: 'Retrieve a single football season by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;stages;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_season') + } + return { + success: true, + output: { + season: data.data ?? null, + }, + } + }, + + outputs: { + season: { + type: 'object', + description: 'The requested season object', + properties: SPORTMONKS_SEASON_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_seasons.ts b/apps/sim/tools/sportmonks_football/get_seasons.ts new file mode 100644 index 00000000000..adc29cea87c --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_seasons.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SEASON_PROPERTIES, + type SportmonksSeason, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetSeasonsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetSeasonsResponse extends ToolResponse { + output: { + seasons: SportmonksSeason[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetSeasonsTool: ToolConfig< + SportmonksGetSeasonsParams, + SportmonksGetSeasonsResponse +> = { + id: 'sportmonks_football_get_seasons', + name: 'Get Seasons', + description: 'Retrieve all football seasons available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;stages)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. seasonLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/seasons`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_seasons') + } + return { + success: true, + output: { + seasons: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + seasons: { + type: 'array', + description: 'Array of season objects', + items: { type: 'object', properties: SPORTMONKS_SEASON_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_seasons_by_team.ts b/apps/sim/tools/sportmonks_football/get_seasons_by_team.ts new file mode 100644 index 00000000000..adcdffc84e7 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_seasons_by_team.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SEASON_PROPERTIES, + type SportmonksSeason, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetSeasonsByTeamParams extends SportmonksBaseParams { + teamId: string +} + +export interface SportmonksGetSeasonsByTeamResponse extends ToolResponse { + output: { + seasons: SportmonksSeason[] + } +} + +export const sportmonksGetSeasonsByTeamTool: ToolConfig< + SportmonksGetSeasonsByTeamParams, + SportmonksGetSeasonsByTeamResponse +> = { + id: 'sportmonks_football_get_seasons_by_team', + name: 'Get Seasons by Team', + description: 'Retrieve all seasons for a team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;stages)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/seasons/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_seasons_by_team') + } + return { + success: true, + output: { + seasons: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + seasons: { + type: 'array', + description: 'Array of season objects for the team', + items: { type: 'object', properties: SPORTMONKS_SEASON_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_stage.ts b/apps/sim/tools/sportmonks_football/get_stage.ts new file mode 100644 index 00000000000..f2ec0f9a4c6 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_stage.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STAGE_PROPERTIES, + type SportmonksStage, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStageParams extends SportmonksBaseParams { + stageId: string +} + +export interface SportmonksGetStageResponse extends ToolResponse { + output: { + stage: SportmonksStage | null + } +} + +export const sportmonksGetStageTool: ToolConfig< + SportmonksGetStageParams, + SportmonksGetStageResponse +> = { + id: 'sportmonks_football_get_stage', + name: 'Get Stage by ID', + description: 'Retrieve a single football stage by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + stageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the stage', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;rounds)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/stages/${encodeURIComponent(params.stageId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stage') + } + return { + success: true, + output: { + stage: data.data ?? null, + }, + } + }, + + outputs: { + stage: { + type: 'object', + description: 'The requested stage object', + properties: SPORTMONKS_STAGE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_stage_statistics.ts b/apps/sim/tools/sportmonks_football/get_stage_statistics.ts new file mode 100644 index 00000000000..d2ce43e3abc --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_stage_statistics.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STATISTIC_PROPERTIES, + type SportmonksStatistic, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStageStatisticsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + stageId: string +} + +export interface SportmonksGetStageStatisticsResponse extends ToolResponse { + output: { + statistics: SportmonksStatistic[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetStageStatisticsTool: ToolConfig< + SportmonksGetStageStatisticsParams, + SportmonksGetStageStatisticsResponse +> = { + id: 'sportmonks_football_get_stage_statistics', + name: 'Get Stage Statistics', + description: 'Retrieve all available statistics for a stage ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + stageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the stage', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. seasonstatisticTypes:52,88)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/statistics/stages/${encodeURIComponent(params.stageId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stage_statistics') + } + return { + success: true, + output: { + statistics: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + statistics: { + type: 'array', + description: 'Array of statistic entries for the stage', + items: { type: 'object', properties: SPORTMONKS_STATISTIC_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_stages.ts b/apps/sim/tools/sportmonks_football/get_stages.ts new file mode 100644 index 00000000000..05dbb99e31a --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_stages.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STAGE_PROPERTIES, + type SportmonksStage, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStagesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetStagesResponse extends ToolResponse { + output: { + stages: SportmonksStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetStagesTool: ToolConfig< + SportmonksGetStagesParams, + SportmonksGetStagesResponse +> = { + id: 'sportmonks_football_get_stages', + name: 'Get Stages', + description: 'Retrieve all football stages available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;rounds)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. stageSeasons:19735)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/stages`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stages') + } + return { + success: true, + output: { + stages: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + stages: { + type: 'array', + description: 'Array of stage objects', + items: { type: 'object', properties: SPORTMONKS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_stages_by_season.ts b/apps/sim/tools/sportmonks_football/get_stages_by_season.ts new file mode 100644 index 00000000000..9e2005cf106 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_stages_by_season.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STAGE_PROPERTIES, + type SportmonksStage, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStagesBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetStagesBySeasonResponse extends ToolResponse { + output: { + stages: SportmonksStage[] + } +} + +export const sportmonksGetStagesBySeasonTool: ToolConfig< + SportmonksGetStagesBySeasonParams, + SportmonksGetStagesBySeasonResponse +> = { + id: 'sportmonks_football_get_stages_by_season', + name: 'Get Stages by Season', + description: 'Retrieve all stages for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;rounds)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/stages/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stages_by_season') + } + return { + success: true, + output: { + stages: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + stages: { + type: 'array', + description: 'Array of stage objects for the season', + items: { type: 'object', properties: SPORTMONKS_STAGE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_standing_corrections_by_season.ts b/apps/sim/tools/sportmonks_football/get_standing_corrections_by_season.ts new file mode 100644 index 00000000000..71144a542de --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_standing_corrections_by_season.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STANDING_CORRECTION_PROPERTIES, + type SportmonksStandingCorrection, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStandingCorrectionsBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetStandingCorrectionsBySeasonResponse extends ToolResponse { + output: { + corrections: SportmonksStandingCorrection[] + } +} + +export const sportmonksGetStandingCorrectionsBySeasonTool: ToolConfig< + SportmonksGetStandingCorrectionsBySeasonParams, + SportmonksGetStandingCorrectionsBySeasonResponse +> = { + id: 'sportmonks_football_get_standing_corrections_by_season', + name: 'Get Standing Corrections by Season', + description: 'Retrieve point corrections (awarded or deducted) for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant;stage)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/standings/corrections/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_standing_corrections_by_season') + } + return { + success: true, + output: { + corrections: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + corrections: { + type: 'array', + description: 'Array of standing correction entries for the season', + items: { type: 'object', properties: SPORTMONKS_STANDING_CORRECTION_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_standings.ts b/apps/sim/tools/sportmonks_football/get_standings.ts new file mode 100644 index 00000000000..75c4bb46deb --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_standings.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STANDING_PROPERTIES, + type SportmonksStanding, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStandingsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetStandingsResponse extends ToolResponse { + output: { + standings: SportmonksStanding[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetStandingsTool: ToolConfig< + SportmonksGetStandingsParams, + SportmonksGetStandingsResponse +> = { + id: 'sportmonks_football_get_standings', + name: 'Get All Standings', + description: 'Retrieve all standings available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;league;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/standings`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_standings') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of standing entries', + items: { type: 'object', properties: SPORTMONKS_STANDING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_standings_by_round.ts b/apps/sim/tools/sportmonks_football/get_standings_by_round.ts new file mode 100644 index 00000000000..d7014d123f1 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_standings_by_round.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STANDING_PROPERTIES, + type SportmonksStanding, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStandingsByRoundParams extends SportmonksBaseParams { + roundId: string +} + +export interface SportmonksGetStandingsByRoundResponse extends ToolResponse { + output: { + standings: SportmonksStanding[] + } +} + +export const sportmonksGetStandingsByRoundTool: ToolConfig< + SportmonksGetStandingsByRoundParams, + SportmonksGetStandingsByRoundResponse +> = { + id: 'sportmonks_football_get_standings_by_round', + name: 'Get Standings by Round', + description: 'Retrieve the full standing table for a round ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + roundId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the round', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. standingGroups:246697)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/standings/rounds/${encodeURIComponent(params.roundId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_standings_by_round') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of standing entries for the round', + items: { type: 'object', properties: SPORTMONKS_STANDING_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_state.ts b/apps/sim/tools/sportmonks_football/get_state.ts new file mode 100644 index 00000000000..d2ae078f157 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_state.ts @@ -0,0 +1,77 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STATE_PROPERTIES, + type SportmonksState, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStateParams extends SportmonksBaseParams { + stateId: string +} + +export interface SportmonksGetStateResponse extends ToolResponse { + output: { + state: SportmonksState | null + } +} + +export const sportmonksGetStateTool: ToolConfig< + SportmonksGetStateParams, + SportmonksGetStateResponse +> = { + id: 'sportmonks_football_get_state', + name: 'Get State by ID', + description: 'Retrieve a single fixture state by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + stateId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the state', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/states/${encodeURIComponent(params.stateId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_state') + } + return { + success: true, + output: { + state: data.data ?? null, + }, + } + }, + + outputs: { + state: { + type: 'object', + description: 'The requested fixture state object', + properties: SPORTMONKS_STATE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_states.ts b/apps/sim/tools/sportmonks_football/get_states.ts new file mode 100644 index 00000000000..e381ff4145e --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_states.ts @@ -0,0 +1,93 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STATE_PROPERTIES, + type SportmonksState, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetStatesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetStatesResponse extends ToolResponse { + output: { + states: SportmonksState[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetStatesTool: ToolConfig< + SportmonksGetStatesParams, + SportmonksGetStatesResponse +> = { + id: 'sportmonks_football_get_states', + name: 'Get States', + description: + 'Retrieve all fixture states (e.g. Not Started, 1st Half, Full Time) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/states`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_states') + } + return { + success: true, + output: { + states: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + states: { + type: 'array', + description: 'Array of fixture state objects', + items: { type: 'object', properties: SPORTMONKS_STATE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_team_rankings.ts b/apps/sim/tools/sportmonks_football/get_team_rankings.ts new file mode 100644 index 00000000000..9978a9fbf4f --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_team_rankings.ts @@ -0,0 +1,98 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_RANKING_PROPERTIES, + type SportmonksTeamRanking, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamRankingsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetTeamRankingsResponse extends ToolResponse { + output: { + teamRankings: SportmonksTeamRanking[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTeamRankingsTool: ToolConfig< + SportmonksGetTeamRankingsParams, + SportmonksGetTeamRankingsResponse +> = { + id: 'sportmonks_football_get_team_rankings', + name: 'Get All Team Rankings', + description: 'Retrieve all team rankings available within your Sportmonks subscription (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. team)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/team-rankings`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_rankings') + } + return { + success: true, + output: { + teamRankings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teamRankings: { + type: 'array', + description: 'Array of team ranking objects', + items: { type: 'object', properties: SPORTMONKS_TEAM_RANKING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_team_rankings_by_date.ts b/apps/sim/tools/sportmonks_football/get_team_rankings_by_date.ts new file mode 100644 index 00000000000..e536d9e4a07 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_team_rankings_by_date.ts @@ -0,0 +1,109 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_RANKING_PROPERTIES, + type SportmonksTeamRanking, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamRankingsByDateParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + date: string +} + +export interface SportmonksGetTeamRankingsByDateResponse extends ToolResponse { + output: { + teamRankings: SportmonksTeamRanking[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTeamRankingsByDateTool: ToolConfig< + SportmonksGetTeamRankingsByDateParams, + SportmonksGetTeamRankingsByDateResponse +> = { + id: 'sportmonks_football_get_team_rankings_by_date', + name: 'Get Team Rankings by Date', + description: 'Retrieve team rankings for a given date (YYYY-MM-DD) from Sportmonks (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + date: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ranking date in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. team)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/team-rankings/date/${encodeURIComponent(params.date.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_rankings_by_date') + } + return { + success: true, + output: { + teamRankings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teamRankings: { + type: 'array', + description: 'Array of team ranking objects for the date', + items: { type: 'object', properties: SPORTMONKS_TEAM_RANKING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_team_rankings_by_team.ts b/apps/sim/tools/sportmonks_football/get_team_rankings_by_team.ts new file mode 100644 index 00000000000..545b8bace7a --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_team_rankings_by_team.ts @@ -0,0 +1,109 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_RANKING_PROPERTIES, + type SportmonksTeamRanking, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamRankingsByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksGetTeamRankingsByTeamResponse extends ToolResponse { + output: { + teamRankings: SportmonksTeamRanking[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTeamRankingsByTeamTool: ToolConfig< + SportmonksGetTeamRankingsByTeamParams, + SportmonksGetTeamRankingsByTeamResponse +> = { + id: 'sportmonks_football_get_team_rankings_by_team', + name: 'Get Team Rankings by Team', + description: 'Retrieve team rankings for a team ID from Sportmonks (beta)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. team)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/team-rankings/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_rankings_by_team') + } + return { + success: true, + output: { + teamRankings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teamRankings: { + type: 'array', + description: 'Array of team ranking objects for the team', + items: { type: 'object', properties: SPORTMONKS_TEAM_RANKING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_team_squad_by_season.ts b/apps/sim/tools/sportmonks_football/get_team_squad_by_season.ts new file mode 100644 index 00000000000..0c71dba378b --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_team_squad_by_season.ts @@ -0,0 +1,98 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SQUAD_PROPERTIES, + type SportmonksSquadEntry, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamSquadBySeasonParams extends SportmonksBaseParams { + seasonId: string + teamId: string +} + +export interface SportmonksGetTeamSquadBySeasonResponse extends ToolResponse { + output: { + squad: SportmonksSquadEntry[] + } +} + +export const sportmonksGetTeamSquadBySeasonTool: ToolConfig< + SportmonksGetTeamSquadBySeasonParams, + SportmonksGetTeamSquadBySeasonResponse +> = { + id: 'sportmonks_football_get_team_squad_by_season', + name: 'Get Team Squad by Season', + description: 'Retrieve the (historical) squad for a team in a specific season from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. player;position)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/squads/seasons/${encodeURIComponent( + params.seasonId.trim() + )}/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_squad_by_season') + } + return { + success: true, + output: { + squad: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + squad: { + type: 'array', + description: 'Array of squad entries for the team in the season', + items: { type: 'object', properties: SPORTMONKS_SQUAD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_teams_by_country.ts b/apps/sim/tools/sportmonks_football/get_teams_by_country.ts new file mode 100644 index 00000000000..e91d1dfad46 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_teams_by_country.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_PROPERTIES, + type SportmonksTeam, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamsByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksGetTeamsByCountryResponse extends ToolResponse { + output: { + teams: SportmonksTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTeamsByCountryTool: ToolConfig< + SportmonksGetTeamsByCountryParams, + SportmonksGetTeamsByCountryResponse +> = { + id: 'sportmonks_football_get_teams_by_country', + name: 'Get Teams by Country', + description: 'Retrieve all teams for a country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;venue)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order teams by id (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/teams/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_teams_by_country') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team objects for the country', + items: { type: 'object', properties: SPORTMONKS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_teams_by_season.ts b/apps/sim/tools/sportmonks_football/get_teams_by_season.ts new file mode 100644 index 00000000000..cb967745c32 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_teams_by_season.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TEAM_PROPERTIES, + type SportmonksTeam, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTeamsBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksGetTeamsBySeasonResponse extends ToolResponse { + output: { + teams: SportmonksTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTeamsBySeasonTool: ToolConfig< + SportmonksGetTeamsBySeasonParams, + SportmonksGetTeamsBySeasonResponse +> = { + id: 'sportmonks_football_get_teams_by_season', + name: 'Get Teams by Season', + description: 'Retrieve all teams for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;venue)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order teams by id (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/teams/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_teams_by_season') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team objects for the season', + items: { type: 'object', properties: SPORTMONKS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_topscorers_by_stage.ts b/apps/sim/tools/sportmonks_football/get_topscorers_by_stage.ts new file mode 100644 index 00000000000..ac6269bafa3 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_topscorers_by_stage.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TOPSCORER_PROPERTIES, + type SportmonksTopscorer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTopscorersByStageParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + stageId: string +} + +export interface SportmonksGetTopscorersByStageResponse extends ToolResponse { + output: { + topscorers: SportmonksTopscorer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTopscorersByStageTool: ToolConfig< + SportmonksGetTopscorersByStageParams, + SportmonksGetTopscorersByStageResponse +> = { + id: 'sportmonks_football_get_topscorers_by_stage', + name: 'Get Topscorers by Stage', + description: 'Retrieve topscorers for a stage by stage ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + stageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the stage', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;participant;type)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. stageTopscorerTypes:208)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/topscorers/stages/${encodeURIComponent(params.stageId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_topscorers_by_stage') + } + return { + success: true, + output: { + topscorers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + topscorers: { + type: 'array', + description: 'Array of topscorer entries for the stage', + items: { type: 'object', properties: SPORTMONKS_TOPSCORER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_totw.ts b/apps/sim/tools/sportmonks_football/get_totw.ts new file mode 100644 index 00000000000..fba74e64640 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_totw.ts @@ -0,0 +1,96 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TOTW_PROPERTIES, + type SportmonksTotw, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTotwParams extends SportmonksBaseParams, SportmonksPaginationParams {} + +export interface SportmonksGetTotwResponse extends ToolResponse { + output: { + totw: SportmonksTotw[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTotwTool: ToolConfig = + { + id: 'sportmonks_football_get_totw', + name: 'Get All Team of the Week', + description: 'Retrieve all available Team of the Week (TOTW) entries from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. fixture;team;player;round)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/team-of-the-week`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_totw') + } + return { + success: true, + output: { + totw: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + totw: { + type: 'array', + description: 'Array of Team of the Week entries', + items: { type: 'object', properties: SPORTMONKS_TOTW_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, + } diff --git a/apps/sim/tools/sportmonks_football/get_totw_by_round.ts b/apps/sim/tools/sportmonks_football/get_totw_by_round.ts new file mode 100644 index 00000000000..53d7b201b80 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_totw_by_round.ts @@ -0,0 +1,84 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TOTW_PROPERTIES, + type SportmonksTotw, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTotwByRoundParams extends SportmonksBaseParams { + roundId: string +} + +export interface SportmonksGetTotwByRoundResponse extends ToolResponse { + output: { + totw: SportmonksTotw[] + } +} + +export const sportmonksGetTotwByRoundTool: ToolConfig< + SportmonksGetTotwByRoundParams, + SportmonksGetTotwByRoundResponse +> = { + id: 'sportmonks_football_get_totw_by_round', + name: 'Get Team of the Week by Round', + description: 'Retrieve the Team of the Week (TOTW) for a round ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + roundId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the round', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. fixture;team;player;round)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/team-of-the-week/rounds/${encodeURIComponent(params.roundId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_totw_by_round') + } + return { + success: true, + output: { + totw: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + totw: { + type: 'array', + description: 'Array of Team of the Week entries for the round', + items: { type: 'object', properties: SPORTMONKS_TOTW_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfer.ts b/apps/sim/tools/sportmonks_football/get_transfer.ts new file mode 100644 index 00000000000..a7c4e933dd4 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfer.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_PROPERTIES, + type SportmonksTransfer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransferParams extends SportmonksBaseParams { + transferId: string +} + +export interface SportmonksGetTransferResponse extends ToolResponse { + output: { + transfer: SportmonksTransfer | null + } +} + +export const sportmonksGetTransferTool: ToolConfig< + SportmonksGetTransferParams, + SportmonksGetTransferResponse +> = { + id: 'sportmonks_football_get_transfer', + name: 'Get Transfer by ID', + description: 'Retrieve a single transfer by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + transferId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the transfer', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfers/${encodeURIComponent(params.transferId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfer') + } + return { + success: true, + output: { + transfer: data.data ?? null, + }, + } + }, + + outputs: { + transfer: { + type: 'object', + description: 'The requested transfer object', + properties: SPORTMONKS_TRANSFER_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfer_rumour.ts b/apps/sim/tools/sportmonks_football/get_transfer_rumour.ts new file mode 100644 index 00000000000..48c3f26b599 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfer_rumour.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES, + type SportmonksTransferRumour, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransferRumourParams extends SportmonksBaseParams { + rumourId: string +} + +export interface SportmonksGetTransferRumourResponse extends ToolResponse { + output: { + transferRumour: SportmonksTransferRumour | null + } +} + +export const sportmonksGetTransferRumourTool: ToolConfig< + SportmonksGetTransferRumourParams, + SportmonksGetTransferRumourResponse +> = { + id: 'sportmonks_football_get_transfer_rumour', + name: 'Get Transfer Rumour by ID', + description: 'Retrieve a single transfer rumour by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + rumourId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the transfer rumour', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfer-rumours/${encodeURIComponent(params.rumourId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfer_rumour') + } + return { + success: true, + output: { + transferRumour: data.data ?? null, + }, + } + }, + + outputs: { + transferRumour: { + type: 'object', + description: 'The requested transfer rumour object', + properties: SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfer_rumours_between_dates.ts b/apps/sim/tools/sportmonks_football/get_transfer_rumours_between_dates.ts new file mode 100644 index 00000000000..5375e5c54a5 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfer_rumours_between_dates.ts @@ -0,0 +1,125 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES, + type SportmonksTransferRumour, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransferRumoursBetweenDatesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + startDate: string + endDate: string +} + +export interface SportmonksGetTransferRumoursBetweenDatesResponse extends ToolResponse { + output: { + transferRumours: SportmonksTransferRumour[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTransferRumoursBetweenDatesTool: ToolConfig< + SportmonksGetTransferRumoursBetweenDatesParams, + SportmonksGetTransferRumoursBetweenDatesResponse +> = { + id: 'sportmonks_football_get_transfer_rumours_between_dates', + name: 'Get Transfer Rumours Between Dates', + description: 'Retrieve transfer rumours within a date range (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + startDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfer-rumours/between/${encodeURIComponent( + params.startDate.trim() + )}/${encodeURIComponent(params.endDate.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfer_rumours_between_dates') + } + return { + success: true, + output: { + transferRumours: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transferRumours: { + type: 'array', + description: 'Array of transfer rumour objects within the date range', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfer_rumours_by_player.ts b/apps/sim/tools/sportmonks_football/get_transfer_rumours_by_player.ts new file mode 100644 index 00000000000..38aa22bd0b5 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfer_rumours_by_player.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES, + type SportmonksTransferRumour, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransferRumoursByPlayerParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + playerId: string +} + +export interface SportmonksGetTransferRumoursByPlayerResponse extends ToolResponse { + output: { + transferRumours: SportmonksTransferRumour[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTransferRumoursByPlayerTool: ToolConfig< + SportmonksGetTransferRumoursByPlayerParams, + SportmonksGetTransferRumoursByPlayerResponse +> = { + id: 'sportmonks_football_get_transfer_rumours_by_player', + name: 'Get Transfer Rumours by Player', + description: 'Retrieve transfer rumours for a player ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + playerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the player', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfer-rumours/players/${encodeURIComponent(params.playerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfer_rumours_by_player') + } + return { + success: true, + output: { + transferRumours: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transferRumours: { + type: 'array', + description: 'Array of transfer rumour objects for the player', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfer_rumours_by_team.ts b/apps/sim/tools/sportmonks_football/get_transfer_rumours_by_team.ts new file mode 100644 index 00000000000..6e18568638d --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfer_rumours_by_team.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES, + type SportmonksTransferRumour, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransferRumoursByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksGetTransferRumoursByTeamResponse extends ToolResponse { + output: { + transferRumours: SportmonksTransferRumour[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTransferRumoursByTeamTool: ToolConfig< + SportmonksGetTransferRumoursByTeamParams, + SportmonksGetTransferRumoursByTeamResponse +> = { + id: 'sportmonks_football_get_transfer_rumours_by_team', + name: 'Get Transfer Rumours by Team', + description: 'Retrieve transfer rumours for a team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfer-rumours/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfer_rumours_by_team') + } + return { + success: true, + output: { + transferRumours: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transferRumours: { + type: 'array', + description: 'Array of transfer rumour objects for the team', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfers_between_dates.ts b/apps/sim/tools/sportmonks_football/get_transfers_between_dates.ts new file mode 100644 index 00000000000..f340a0232d4 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfers_between_dates.ts @@ -0,0 +1,125 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_PROPERTIES, + type SportmonksTransfer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransfersBetweenDatesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + startDate: string + endDate: string +} + +export interface SportmonksGetTransfersBetweenDatesResponse extends ToolResponse { + output: { + transfers: SportmonksTransfer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTransfersBetweenDatesTool: ToolConfig< + SportmonksGetTransfersBetweenDatesParams, + SportmonksGetTransfersBetweenDatesResponse +> = { + id: 'sportmonks_football_get_transfers_between_dates', + name: 'Get Transfers Between Dates', + description: 'Retrieve transfers within a date range (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + startDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. transferTypes:219,220)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfers/between/${encodeURIComponent( + params.startDate.trim() + )}/${encodeURIComponent(params.endDate.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfers_between_dates') + } + return { + success: true, + output: { + transfers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transfers: { + type: 'array', + description: 'Array of transfer objects within the date range', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfers_by_player.ts b/apps/sim/tools/sportmonks_football/get_transfers_by_player.ts new file mode 100644 index 00000000000..80aef172eff --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfers_by_player.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_PROPERTIES, + type SportmonksTransfer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransfersByPlayerParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + playerId: string +} + +export interface SportmonksGetTransfersByPlayerResponse extends ToolResponse { + output: { + transfers: SportmonksTransfer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTransfersByPlayerTool: ToolConfig< + SportmonksGetTransfersByPlayerParams, + SportmonksGetTransfersByPlayerResponse +> = { + id: 'sportmonks_football_get_transfers_by_player', + name: 'Get Transfers by Player', + description: 'Retrieve transfers for a player by player ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + playerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the player', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. fromTeam;toTeam;type)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfers/players/${encodeURIComponent(params.playerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfers_by_player') + } + return { + success: true, + output: { + transfers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transfers: { + type: 'array', + description: 'Array of transfer objects for the player', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_transfers_by_team.ts b/apps/sim/tools/sportmonks_football/get_transfers_by_team.ts new file mode 100644 index 00000000000..313b4d42f92 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_transfers_by_team.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TRANSFER_PROPERTIES, + type SportmonksTransfer, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTransfersByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksGetTransfersByTeamResponse extends ToolResponse { + output: { + transfers: SportmonksTransfer[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTransfersByTeamTool: ToolConfig< + SportmonksGetTransfersByTeamParams, + SportmonksGetTransfersByTeamResponse +> = { + id: 'sportmonks_football_get_transfers_by_team', + name: 'Get Transfers by Team', + description: 'Retrieve transfers for a team by team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. player;fromTeam;toTeam)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. transferTypes:219,220)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/transfers/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_transfers_by_team') + } + return { + success: true, + output: { + transfers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + transfers: { + type: 'array', + description: 'Array of transfer objects for the team', + items: { type: 'object', properties: SPORTMONKS_TRANSFER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_tv_station.ts b/apps/sim/tools/sportmonks_football/get_tv_station.ts new file mode 100644 index 00000000000..a68c46cd5d8 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_tv_station.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TVSTATION_PROPERTIES, + type SportmonksTVStation, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTvStationParams extends SportmonksBaseParams { + tvStationId: string +} + +export interface SportmonksGetTvStationResponse extends ToolResponse { + output: { + tvStation: SportmonksTVStation | null + } +} + +export const sportmonksGetTvStationTool: ToolConfig< + SportmonksGetTvStationParams, + SportmonksGetTvStationResponse +> = { + id: 'sportmonks_football_get_tv_station', + name: 'Get TV Station by ID', + description: 'Retrieve a single TV station by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + tvStationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the TV station', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/tv-stations/${encodeURIComponent(params.tvStationId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_tv_station') + } + return { + success: true, + output: { + tvStation: data.data ?? null, + }, + } + }, + + outputs: { + tvStation: { + type: 'object', + description: 'The requested TV station object', + properties: SPORTMONKS_TVSTATION_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_tv_stations.ts b/apps/sim/tools/sportmonks_football/get_tv_stations.ts new file mode 100644 index 00000000000..fa405de3a92 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_tv_stations.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TVSTATION_PROPERTIES, + type SportmonksTVStation, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTvStationsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetTvStationsResponse extends ToolResponse { + output: { + tvStations: SportmonksTVStation[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTvStationsTool: ToolConfig< + SportmonksGetTvStationsParams, + SportmonksGetTvStationsResponse +> = { + id: 'sportmonks_football_get_tv_stations', + name: 'Get TV Stations', + description: 'Retrieve all TV stations available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/tv-stations`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_tv_stations') + } + return { + success: true, + output: { + tvStations: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + tvStations: { + type: 'array', + description: 'Array of TV station objects', + items: { type: 'object', properties: SPORTMONKS_TVSTATION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_tv_stations_by_fixture.ts b/apps/sim/tools/sportmonks_football/get_tv_stations_by_fixture.ts new file mode 100644 index 00000000000..024ebe66128 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_tv_stations_by_fixture.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_TVSTATION_PROPERTIES, + type SportmonksTVStation, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetTvStationsByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetTvStationsByFixtureResponse extends ToolResponse { + output: { + tvStations: SportmonksTVStation[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetTvStationsByFixtureTool: ToolConfig< + SportmonksGetTvStationsByFixtureParams, + SportmonksGetTvStationsByFixtureResponse +> = { + id: 'sportmonks_football_get_tv_stations_by_fixture', + name: 'Get TV Stations by Fixture', + description: 'Retrieve broadcasting TV stations for a fixture by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. fixtures;countries)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/tv-stations/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_tv_stations_by_fixture') + } + return { + success: true, + output: { + tvStations: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + tvStations: { + type: 'array', + description: 'Array of TV station objects broadcasting the fixture', + items: { type: 'object', properties: SPORTMONKS_TVSTATION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_market.ts b/apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_market.ts new file mode 100644 index 00000000000..34a8bbcf47b --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_market.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetUpcomingFixturesByMarketParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + marketId: string +} + +export interface SportmonksGetUpcomingFixturesByMarketResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetUpcomingFixturesByMarketTool: ToolConfig< + SportmonksGetUpcomingFixturesByMarketParams, + SportmonksGetUpcomingFixturesByMarketResponse +> = { + id: 'sportmonks_football_get_upcoming_fixtures_by_market', + name: 'Get Upcoming Fixtures by Market', + description: 'Retrieve all upcoming fixtures for a market ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + marketId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the market', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participants;odds)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/upcoming/markets/${encodeURIComponent(params.marketId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_upcoming_fixtures_by_market') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of upcoming fixture objects for the market', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_tv_station.ts b/apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_tv_station.ts new file mode 100644 index 00000000000..6c6197e32b9 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_upcoming_fixtures_by_tv_station.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetUpcomingFixturesByTvStationParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + tvStationId: string +} + +export interface SportmonksGetUpcomingFixturesByTvStationResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetUpcomingFixturesByTvStationTool: ToolConfig< + SportmonksGetUpcomingFixturesByTvStationParams, + SportmonksGetUpcomingFixturesByTvStationResponse +> = { + id: 'sportmonks_football_get_upcoming_fixtures_by_tv_station', + name: 'Get Upcoming Fixtures by TV Station', + description: 'Retrieve all upcoming fixtures available for a TV station ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + tvStationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the TV station', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participants)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/upcoming/tv-stations/${encodeURIComponent(params.tvStationId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_upcoming_fixtures_by_tv_station') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of upcoming fixture objects for the TV station', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_value_bets.ts b/apps/sim/tools/sportmonks_football/get_value_bets.ts new file mode 100644 index 00000000000..1d888525516 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_value_bets.ts @@ -0,0 +1,99 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PREDICTION_PROPERTIES, + type SportmonksPrediction, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetValueBetsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetValueBetsResponse extends ToolResponse { + output: { + valueBets: SportmonksPrediction[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetValueBetsTool: ToolConfig< + SportmonksGetValueBetsParams, + SportmonksGetValueBetsResponse +> = { + id: 'sportmonks_football_get_value_bets', + name: 'Get Value Bets', + description: 'Retrieve all value bets available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;fixture)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/value-bets`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_value_bets') + } + return { + success: true, + output: { + valueBets: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + valueBets: { + type: 'array', + description: 'Array of value bet prediction objects', + items: { type: 'object', properties: SPORTMONKS_PREDICTION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_value_bets_by_fixture.ts b/apps/sim/tools/sportmonks_football/get_value_bets_by_fixture.ts new file mode 100644 index 00000000000..fc53e6f2764 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_value_bets_by_fixture.ts @@ -0,0 +1,109 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_PREDICTION_PROPERTIES, + type SportmonksPrediction, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetValueBetsByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetValueBetsByFixtureResponse extends ToolResponse { + output: { + valueBets: SportmonksPrediction[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetValueBetsByFixtureTool: ToolConfig< + SportmonksGetValueBetsByFixtureParams, + SportmonksGetValueBetsByFixtureResponse +> = { + id: 'sportmonks_football_get_value_bets_by_fixture', + name: 'Get Value Bets by Fixture', + description: 'Retrieve value bet predictions for a fixture by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. type;fixture)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/predictions/value-bets/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_value_bets_by_fixture') + } + return { + success: true, + output: { + valueBets: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + valueBets: { + type: 'array', + description: 'Array of value bet prediction entries for the fixture', + items: { type: 'object', properties: SPORTMONKS_PREDICTION_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_venue.ts b/apps/sim/tools/sportmonks_football/get_venue.ts new file mode 100644 index 00000000000..02bda7862ef --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_venue.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_VENUE_PROPERTIES, + type SportmonksVenue, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetVenueParams extends SportmonksBaseParams { + venueId: string +} + +export interface SportmonksGetVenueResponse extends ToolResponse { + output: { + venue: SportmonksVenue | null + } +} + +export const sportmonksGetVenueTool: ToolConfig< + SportmonksGetVenueParams, + SportmonksGetVenueResponse +> = { + id: 'sportmonks_football_get_venue', + name: 'Get Venue by ID', + description: 'Retrieve a single football venue by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + venueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the venue', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;city;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/venues/${encodeURIComponent(params.venueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_venue') + } + return { + success: true, + output: { + venue: data.data ?? null, + }, + } + }, + + outputs: { + venue: { + type: 'object', + description: 'The requested venue object', + properties: SPORTMONKS_VENUE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_venues.ts b/apps/sim/tools/sportmonks_football/get_venues.ts new file mode 100644 index 00000000000..3e8466339b1 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_venues.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_VENUE_PROPERTIES, + type SportmonksVenue, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetVenuesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetVenuesResponse extends ToolResponse { + output: { + venues: SportmonksVenue[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksGetVenuesTool: ToolConfig< + SportmonksGetVenuesParams, + SportmonksGetVenuesResponse +> = { + id: 'sportmonks_football_get_venues', + name: 'Get Venues', + description: 'Retrieve all football venues available within your Sportmonks subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. venueCountries:98)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_BASE_URL}/venues`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_venues') + } + return { + success: true, + output: { + venues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + venues: { + type: 'array', + description: 'Array of venue objects', + items: { type: 'object', properties: SPORTMONKS_VENUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/get_venues_by_season.ts b/apps/sim/tools/sportmonks_football/get_venues_by_season.ts new file mode 100644 index 00000000000..b6fa4f875eb --- /dev/null +++ b/apps/sim/tools/sportmonks_football/get_venues_by_season.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_VENUE_PROPERTIES, + type SportmonksVenue, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetVenuesBySeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksGetVenuesBySeasonResponse extends ToolResponse { + output: { + venues: SportmonksVenue[] + } +} + +export const sportmonksGetVenuesBySeasonTool: ToolConfig< + SportmonksGetVenuesBySeasonParams, + SportmonksGetVenuesBySeasonResponse +> = { + id: 'sportmonks_football_get_venues_by_season', + name: 'Get Venues by Season', + description: 'Retrieve all venues for a season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/venues/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_venues_by_season') + } + return { + success: true, + output: { + venues: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + venues: { + type: 'array', + description: 'Array of venue objects for the season', + items: { type: 'object', properties: SPORTMONKS_VENUE_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_football/index.ts b/apps/sim/tools/sportmonks_football/index.ts index 1b07764325d..1fe0bba775f 100644 --- a/apps/sim/tools/sportmonks_football/index.ts +++ b/apps/sim/tools/sportmonks_football/index.ts @@ -1,15 +1,122 @@ +export { sportmonksExpectedByPlayerTool } from './expected_by_player' +export { sportmonksExpectedByTeamTool } from './expected_by_team' +export { sportmonksGetAllCommentariesTool } from './get_all_commentaries' +export { sportmonksGetAllFixturesTool } from './get_all_fixtures' +export { sportmonksGetAllPlayersTool } from './get_all_players' +export { sportmonksGetAllRivalsTool } from './get_all_rivals' +export { sportmonksGetAllTeamsTool } from './get_all_teams' +export { sportmonksGetAllTransferRumoursTool } from './get_all_transfer_rumours' +export { sportmonksGetAllTransfersTool } from './get_all_transfers' +export { sportmonksGetBracketsBySeasonTool } from './get_brackets_by_season' +export { sportmonksGetCoachTool } from './get_coach' +export { sportmonksGetCoachesTool } from './get_coaches' +export { sportmonksGetCoachesByCountryTool } from './get_coaches_by_country' +export { sportmonksGetCommentariesByFixtureTool } from './get_commentaries_by_fixture' +export { sportmonksGetCurrentLeaguesByTeamTool } from './get_current_leagues_by_team' +export { sportmonksGetExpectedLineupsByPlayerTool } from './get_expected_lineups_by_player' +export { sportmonksGetExpectedLineupsByTeamTool } from './get_expected_lineups_by_team' +export { sportmonksGetExtendedTeamSquadTool } from './get_extended_team_squad' export { sportmonksGetFixtureTool } from './get_fixture' export { sportmonksGetFixturesByDateTool } from './get_fixtures_by_date' export { sportmonksGetFixturesByDateRangeTool } from './get_fixtures_by_date_range' +export { sportmonksGetFixturesByDateRangeForTeamTool } from './get_fixtures_by_date_range_for_team' +export { sportmonksGetFixturesByIdsTool } from './get_fixtures_by_ids' +export { sportmonksGetGroupedStandingsByRoundTool } from './get_grouped_standings_by_round' export { sportmonksGetHeadToHeadTool } from './get_head_to_head' export { sportmonksGetInplayLivescoresTool } from './get_inplay_livescores' +export { sportmonksGetLatestCoachesTool } from './get_latest_coaches' +export { sportmonksGetLatestFixturesTool } from './get_latest_fixtures' +export { sportmonksGetLatestLivescoresTool } from './get_latest_livescores' +export { sportmonksGetLatestPlayersTool } from './get_latest_players' +export { sportmonksGetLatestTotwTool } from './get_latest_totw' +export { sportmonksGetLatestTransfersTool } from './get_latest_transfers' export { sportmonksGetLeagueTool } from './get_league' export { sportmonksGetLeaguesTool } from './get_leagues' +export { sportmonksGetLeaguesByCountryTool } from './get_leagues_by_country' +export { sportmonksGetLeaguesByDateTool } from './get_leagues_by_date' +export { sportmonksGetLeaguesByTeamTool } from './get_leagues_by_team' +export { sportmonksGetLiveLeaguesTool } from './get_live_leagues' +export { sportmonksGetLiveProbabilitiesTool } from './get_live_probabilities' +export { sportmonksGetLiveProbabilitiesByFixtureTool } from './get_live_probabilities_by_fixture' +export { sportmonksGetLiveStandingsByLeagueTool } from './get_live_standings_by_league' export { sportmonksGetLivescoresTool } from './get_livescores' +export { sportmonksGetMatchFactsTool } from './get_match_facts' +export { sportmonksGetMatchFactsByDateRangeTool } from './get_match_facts_by_date_range' +export { sportmonksGetMatchFactsByFixtureTool } from './get_match_facts_by_fixture' +export { sportmonksGetMatchFactsByLeagueTool } from './get_match_facts_by_league' +export { sportmonksGetPastFixturesByTvStationTool } from './get_past_fixtures_by_tv_station' export { sportmonksGetPlayerTool } from './get_player' +export { sportmonksGetPlayersByCountryTool } from './get_players_by_country' +export { sportmonksGetPostmatchNewsTool } from './get_postmatch_news' +export { sportmonksGetPostmatchNewsBySeasonTool } from './get_postmatch_news_by_season' +export { sportmonksGetPredictabilityByLeagueTool } from './get_predictability_by_league' +export { sportmonksGetPrematchNewsTool } from './get_prematch_news' +export { sportmonksGetPrematchNewsBySeasonTool } from './get_prematch_news_by_season' +export { sportmonksGetPrematchNewsUpcomingTool } from './get_prematch_news_upcoming' +export { sportmonksGetProbabilitiesTool } from './get_probabilities' +export { sportmonksGetProbabilitiesByFixtureTool } from './get_probabilities_by_fixture' +export { sportmonksGetRefereeTool } from './get_referee' +export { sportmonksGetRefereesTool } from './get_referees' +export { sportmonksGetRefereesByCountryTool } from './get_referees_by_country' +export { sportmonksGetRefereesBySeasonTool } from './get_referees_by_season' +export { sportmonksGetRivalsByTeamTool } from './get_rivals_by_team' +export { sportmonksGetRoundTool } from './get_round' +export { sportmonksGetRoundStatisticsTool } from './get_round_statistics' +export { sportmonksGetRoundsTool } from './get_rounds' +export { sportmonksGetRoundsBySeasonTool } from './get_rounds_by_season' +export { sportmonksGetSchedulesBySeasonTool } from './get_schedules_by_season' +export { sportmonksGetSchedulesBySeasonAndTeamTool } from './get_schedules_by_season_and_team' +export { sportmonksGetSchedulesByTeamTool } from './get_schedules_by_team' +export { sportmonksGetSeasonTool } from './get_season' +export { sportmonksGetSeasonsTool } from './get_seasons' +export { sportmonksGetSeasonsByTeamTool } from './get_seasons_by_team' +export { sportmonksGetStageTool } from './get_stage' +export { sportmonksGetStageStatisticsTool } from './get_stage_statistics' +export { sportmonksGetStagesTool } from './get_stages' +export { sportmonksGetStagesBySeasonTool } from './get_stages_by_season' +export { sportmonksGetStandingCorrectionsBySeasonTool } from './get_standing_corrections_by_season' +export { sportmonksGetStandingsTool } from './get_standings' +export { sportmonksGetStandingsByRoundTool } from './get_standings_by_round' export { sportmonksGetStandingsBySeasonTool } from './get_standings_by_season' +export { sportmonksGetStateTool } from './get_state' +export { sportmonksGetStatesTool } from './get_states' export { sportmonksGetTeamTool } from './get_team' +export { sportmonksGetTeamRankingsTool } from './get_team_rankings' +export { sportmonksGetTeamRankingsByDateTool } from './get_team_rankings_by_date' +export { sportmonksGetTeamRankingsByTeamTool } from './get_team_rankings_by_team' export { sportmonksGetTeamSquadTool } from './get_team_squad' +export { sportmonksGetTeamSquadBySeasonTool } from './get_team_squad_by_season' +export { sportmonksGetTeamsByCountryTool } from './get_teams_by_country' +export { sportmonksGetTeamsBySeasonTool } from './get_teams_by_season' export { sportmonksGetTopscorersBySeasonTool } from './get_topscorers_by_season' +export { sportmonksGetTopscorersByStageTool } from './get_topscorers_by_stage' +export { sportmonksGetTotwTool } from './get_totw' +export { sportmonksGetTotwByRoundTool } from './get_totw_by_round' +export { sportmonksGetTransferTool } from './get_transfer' +export { sportmonksGetTransferRumourTool } from './get_transfer_rumour' +export { sportmonksGetTransferRumoursBetweenDatesTool } from './get_transfer_rumours_between_dates' +export { sportmonksGetTransferRumoursByPlayerTool } from './get_transfer_rumours_by_player' +export { sportmonksGetTransferRumoursByTeamTool } from './get_transfer_rumours_by_team' +export { sportmonksGetTransfersBetweenDatesTool } from './get_transfers_between_dates' +export { sportmonksGetTransfersByPlayerTool } from './get_transfers_by_player' +export { sportmonksGetTransfersByTeamTool } from './get_transfers_by_team' +export { sportmonksGetTvStationTool } from './get_tv_station' +export { sportmonksGetTvStationsTool } from './get_tv_stations' +export { sportmonksGetTvStationsByFixtureTool } from './get_tv_stations_by_fixture' +export { sportmonksGetUpcomingFixturesByMarketTool } from './get_upcoming_fixtures_by_market' +export { sportmonksGetUpcomingFixturesByTvStationTool } from './get_upcoming_fixtures_by_tv_station' +export { sportmonksGetValueBetsTool } from './get_value_bets' +export { sportmonksGetValueBetsByFixtureTool } from './get_value_bets_by_fixture' +export { sportmonksGetVenueTool } from './get_venue' +export { sportmonksGetVenuesTool } from './get_venues' +export { sportmonksGetVenuesBySeasonTool } from './get_venues_by_season' +export { sportmonksSearchCoachesTool } from './search_coaches' +export { sportmonksSearchFixturesTool } from './search_fixtures' +export { sportmonksSearchLeaguesTool } from './search_leagues' export { sportmonksSearchPlayersTool } from './search_players' +export { sportmonksSearchRefereesTool } from './search_referees' +export { sportmonksSearchRoundsTool } from './search_rounds' +export { sportmonksSearchSeasonsTool } from './search_seasons' +export { sportmonksSearchStagesTool } from './search_stages' export { sportmonksSearchTeamsTool } from './search_teams' +export { sportmonksSearchVenuesTool } from './search_venues' diff --git a/apps/sim/tools/sportmonks_football/search_coaches.ts b/apps/sim/tools/sportmonks_football/search_coaches.ts new file mode 100644 index 00000000000..d1453797314 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_coaches.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_COACH_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksCoach, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchCoachesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchCoachesResponse extends ToolResponse { + output: { + coaches: SportmonksCoach[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchCoachesTool: ToolConfig< + SportmonksSearchCoachesParams, + SportmonksSearchCoachesResponse +> = { + id: 'sportmonks_football_search_coaches', + name: 'Search Coaches', + description: 'Search for football coaches by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The coach name to search for (e.g. Gerrard)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/coaches/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_coaches') + } + return { + success: true, + output: { + coaches: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + coaches: { + type: 'array', + description: 'Array of coach objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_COACH_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_fixtures.ts b/apps/sim/tools/sportmonks_football/search_fixtures.ts new file mode 100644 index 00000000000..287dd254eee --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_fixtures.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FIXTURE_PROPERTIES, + SPORTMONKS_FOOTBALL_BASE_URL, + type SportmonksFixture, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchFixturesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchFixturesResponse extends ToolResponse { + output: { + fixtures: SportmonksFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchFixturesTool: ToolConfig< + SportmonksSearchFixturesParams, + SportmonksSearchFixturesResponse +> = { + id: 'sportmonks_football_search_fixtures', + name: 'Search Fixtures', + description: 'Search for football fixtures by name (participants) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The fixture name to search for (e.g. Celtic)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;scores)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. fixtureLeagues:501)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/fixtures/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_fixtures') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of fixture objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_leagues.ts b/apps/sim/tools/sportmonks_football/search_leagues.ts new file mode 100644 index 00000000000..908159d87aa --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_leagues.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_LEAGUE_PROPERTIES, + type SportmonksLeague, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchLeaguesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchLeaguesResponse extends ToolResponse { + output: { + leagues: SportmonksLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchLeaguesTool: ToolConfig< + SportmonksSearchLeaguesParams, + SportmonksSearchLeaguesResponse +> = { + id: 'sportmonks_football_search_leagues', + name: 'Search Leagues', + description: 'Search for football leagues by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The league name to search for (e.g. Premier)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. country;currentSeason)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order leagues (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/leagues/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_leagues') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_referees.ts b/apps/sim/tools/sportmonks_football/search_referees.ts new file mode 100644 index 00000000000..1e2545dcb9c --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_referees.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_REFEREE_PROPERTIES, + type SportmonksReferee, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchRefereesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchRefereesResponse extends ToolResponse { + output: { + referees: SportmonksReferee[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchRefereesTool: ToolConfig< + SportmonksSearchRefereesParams, + SportmonksSearchRefereesResponse +> = { + id: 'sportmonks_football_search_referees', + name: 'Search Referees', + description: 'Search for football referees by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The referee name to search for', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/referees/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_referees') + } + return { + success: true, + output: { + referees: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + referees: { + type: 'array', + description: 'Array of referee objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_REFEREE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_rounds.ts b/apps/sim/tools/sportmonks_football/search_rounds.ts new file mode 100644 index 00000000000..35435ea752a --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_rounds.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_ROUND_PROPERTIES, + type SportmonksRound, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchRoundsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchRoundsResponse extends ToolResponse { + output: { + rounds: SportmonksRound[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchRoundsTool: ToolConfig< + SportmonksSearchRoundsParams, + SportmonksSearchRoundsResponse +> = { + id: 'sportmonks_football_search_rounds', + name: 'Search Rounds', + description: 'Search for football rounds by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The round name to search for (e.g. 5)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/rounds/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_rounds') + } + return { + success: true, + output: { + rounds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + rounds: { + type: 'array', + description: 'Array of round objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_ROUND_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_seasons.ts b/apps/sim/tools/sportmonks_football/search_seasons.ts new file mode 100644 index 00000000000..72201dd94d3 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_seasons.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_SEASON_PROPERTIES, + type SportmonksSeason, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchSeasonsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchSeasonsResponse extends ToolResponse { + output: { + seasons: SportmonksSeason[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchSeasonsTool: ToolConfig< + SportmonksSearchSeasonsParams, + SportmonksSearchSeasonsResponse +> = { + id: 'sportmonks_football_search_seasons', + name: 'Search Seasons', + description: 'Search for football seasons by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The season name to search for (e.g. 2023/2024)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/seasons/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_seasons') + } + return { + success: true, + output: { + seasons: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + seasons: { + type: 'array', + description: 'Array of season objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_SEASON_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_stages.ts b/apps/sim/tools/sportmonks_football/search_stages.ts new file mode 100644 index 00000000000..af7d78fa74f --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_stages.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_STAGE_PROPERTIES, + type SportmonksStage, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchStagesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchStagesResponse extends ToolResponse { + output: { + stages: SportmonksStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchStagesTool: ToolConfig< + SportmonksSearchStagesParams, + SportmonksSearchStagesResponse +> = { + id: 'sportmonks_football_search_stages', + name: 'Search Stages', + description: 'Search for football stages by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The stage name to search for (e.g. Group)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/stages/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_stages') + } + return { + success: true, + output: { + stages: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + stages: { + type: 'array', + description: 'Array of stage objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/search_venues.ts b/apps/sim/tools/sportmonks_football/search_venues.ts new file mode 100644 index 00000000000..24f9d7f8577 --- /dev/null +++ b/apps/sim/tools/sportmonks_football/search_venues.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_BASE_URL, + SPORTMONKS_VENUE_PROPERTIES, + type SportmonksVenue, +} from '@/tools/sportmonks_football/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksSearchVenuesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksSearchVenuesResponse extends ToolResponse { + output: { + venues: SportmonksVenue[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksSearchVenuesTool: ToolConfig< + SportmonksSearchVenuesParams, + SportmonksSearchVenuesResponse +> = { + id: 'sportmonks_football_search_venues', + name: 'Search Venues', + description: 'Search for football venues by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The venue name to search for (e.g. Celtic Park)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_BASE_URL}/venues/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_venues') + } + return { + success: true, + output: { + venues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + venues: { + type: 'array', + description: 'Array of venue objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_VENUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_football/types.ts b/apps/sim/tools/sportmonks_football/types.ts index 47343ec28f4..1c36b8797d1 100644 --- a/apps/sim/tools/sportmonks_football/types.ts +++ b/apps/sim/tools/sportmonks_football/types.ts @@ -223,7 +223,11 @@ export const SPORTMONKS_STANDING_PROPERTIES = { */ export const SPORTMONKS_TOPSCORER_PROPERTIES = { id: { type: 'number', description: 'Unique id of the topscorer record' }, - season_id: { type: 'number', description: 'Season related to the topscorer' }, + season_id: { + type: 'number', + description: 'Season related to the topscorer (absent on stage topscorers)', + optional: true, + }, league_id: { type: 'number', description: 'League related to the topscorer', optional: true }, stage_id: { type: 'number', description: 'Stage related to the topscorer', optional: true }, player_id: { type: 'number', description: 'Player related to the topscorer' }, @@ -369,7 +373,7 @@ export interface SportmonksStanding { export interface SportmonksTopscorer { id: number - season_id: number + season_id?: number league_id?: number stage_id?: number player_id: number @@ -390,3 +394,955 @@ export interface SportmonksSquadEntry { start?: string | null end?: string | null } + +/** + * Output property definitions for a Season object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/league-season-schedule-stage-and-round + */ +export const SPORTMONKS_SEASON_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the season' }, + sport_id: { type: 'number', description: 'Sport of the season' }, + league_id: { type: 'number', description: 'League of the season' }, + tie_breaker_rule_id: { + type: 'number', + description: 'Tie-breaker rule of the season', + nullable: true, + optional: true, + }, + name: { type: 'string', description: 'Name of the season (e.g. 2023/2024)' }, + finished: { type: 'boolean', description: 'Whether the season is finished', optional: true }, + pending: { type: 'boolean', description: 'Whether the season is pending', optional: true }, + is_current: { + type: 'boolean', + description: 'Whether the season is the current season', + optional: true, + }, + standing_method: { + type: 'string', + description: 'Standing calculation method', + nullable: true, + optional: true, + }, + starting_at: { + type: 'string', + description: 'Start date of the season', + nullable: true, + optional: true, + }, + ending_at: { + type: 'string', + description: 'End date of the season', + nullable: true, + optional: true, + }, + standings_recalculated_at: { + type: 'string', + description: 'Last standings recalculation time', + nullable: true, + optional: true, + }, + games_in_current_week: { + type: 'boolean', + description: 'Whether the season has fixtures this week', + optional: true, + }, +} as const satisfies Record + +/** Output property definitions for a Stage object. */ +export const SPORTMONKS_STAGE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the stage' }, + sport_id: { type: 'number', description: 'Sport of the stage' }, + league_id: { type: 'number', description: 'League of the stage' }, + season_id: { type: 'number', description: 'Season of the stage' }, + type_id: { type: 'number', description: 'Type of the stage' }, + name: { type: 'string', description: 'Name of the stage' }, + sort_order: { type: 'number', description: 'Sort order of the stage', optional: true }, + finished: { type: 'boolean', description: 'Whether the stage is finished', optional: true }, + is_current: { + type: 'boolean', + description: 'Whether the stage is the current stage', + optional: true, + }, + starting_at: { + type: 'string', + description: 'Start date of the stage', + nullable: true, + optional: true, + }, + ending_at: { + type: 'string', + description: 'End date of the stage', + nullable: true, + optional: true, + }, + games_in_current_week: { + type: 'boolean', + description: 'Whether the stage has fixtures this week', + optional: true, + }, +} as const satisfies Record + +/** Output property definitions for a Round object. */ +export const SPORTMONKS_ROUND_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the round' }, + sport_id: { type: 'number', description: 'Sport of the round' }, + league_id: { type: 'number', description: 'League of the round' }, + season_id: { type: 'number', description: 'Season of the round' }, + stage_id: { type: 'number', description: 'Stage of the round', nullable: true, optional: true }, + name: { type: 'string', description: 'Name of the round' }, + finished: { type: 'boolean', description: 'Whether the round is finished', optional: true }, + is_current: { + type: 'boolean', + description: 'Whether the round is the current round', + optional: true, + }, + starting_at: { + type: 'string', + description: 'Start date of the round', + nullable: true, + optional: true, + }, + ending_at: { + type: 'string', + description: 'End date of the round', + nullable: true, + optional: true, + }, + games_in_current_week: { + type: 'boolean', + description: 'Whether the round has fixtures this week', + optional: true, + }, +} as const satisfies Record + +/** Output property definitions for a Coach object. */ +export const SPORTMONKS_COACH_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the coach' }, + player_id: { + type: 'number', + description: 'Player related to the coach', + nullable: true, + optional: true, + }, + sport_id: { type: 'number', description: 'Sport of the coach' }, + country_id: { type: 'number', description: 'Country of the coach', nullable: true }, + nationality_id: { type: 'number', description: 'Nationality of the coach', nullable: true }, + city_id: { + type: 'number', + description: 'Birth city of the coach', + nullable: true, + optional: true, + }, + common_name: { type: 'string', description: 'Common name of the coach', optional: true }, + firstname: { + type: 'string', + description: 'First name of the coach', + nullable: true, + optional: true, + }, + lastname: { + type: 'string', + description: 'Last name of the coach', + nullable: true, + optional: true, + }, + name: { type: 'string', description: 'Name of the coach' }, + display_name: { type: 'string', description: 'Display name of the coach', optional: true }, + image_path: { type: 'string', description: 'URL to the coach headshot', optional: true }, + height: { + type: 'number', + description: 'Height of the coach in cm', + nullable: true, + optional: true, + }, + weight: { + type: 'number', + description: 'Weight of the coach in kg', + nullable: true, + optional: true, + }, + date_of_birth: { + type: 'string', + description: 'Date of birth of the coach', + nullable: true, + optional: true, + }, + gender: { type: 'string', description: 'Gender of the coach', nullable: true, optional: true }, +} as const satisfies Record + +/** Output property definitions for a Referee object. */ +export const SPORTMONKS_REFEREE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the referee' }, + sport_id: { type: 'number', description: 'Sport of the referee' }, + country_id: { type: 'number', description: 'Country of the referee', nullable: true }, + nationality_id: { + type: 'number', + description: 'Nationality of the referee', + nullable: true, + optional: true, + }, + city_id: { + type: 'number', + description: 'Birth city of the referee', + nullable: true, + optional: true, + }, + common_name: { type: 'string', description: 'Common name of the referee', optional: true }, + firstname: { + type: 'string', + description: 'First name of the referee', + nullable: true, + optional: true, + }, + lastname: { + type: 'string', + description: 'Last name of the referee', + nullable: true, + optional: true, + }, + name: { type: 'string', description: 'Name of the referee' }, + display_name: { type: 'string', description: 'Display name of the referee', optional: true }, + image_path: { type: 'string', description: 'URL to the referee headshot', optional: true }, + height: { + type: 'number', + description: 'Height of the referee in cm', + nullable: true, + optional: true, + }, + weight: { + type: 'number', + description: 'Weight of the referee in kg', + nullable: true, + optional: true, + }, + date_of_birth: { + type: 'string', + description: 'Date of birth of the referee', + nullable: true, + optional: true, + }, + gender: { type: 'string', description: 'Gender of the referee', nullable: true, optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Venue object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_VENUE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the venue' }, + country_id: { type: 'number', description: 'Country of the venue', nullable: true }, + city_id: { type: 'number', description: 'City of the venue', nullable: true, optional: true }, + name: { type: 'string', description: 'Name of the venue' }, + address: { type: 'string', description: 'Address of the venue', nullable: true, optional: true }, + zipcode: { type: 'string', description: 'Zipcode of the venue', nullable: true, optional: true }, + latitude: { + type: 'string', + description: 'Latitude of the venue', + nullable: true, + optional: true, + }, + longitude: { + type: 'string', + description: 'Longitude of the venue', + nullable: true, + optional: true, + }, + capacity: { + type: 'number', + description: 'Seating capacity of the venue', + nullable: true, + optional: true, + }, + image_path: { + type: 'string', + description: 'Image path of the venue', + nullable: true, + optional: true, + }, + city_name: { + type: 'string', + description: 'Name of the city the venue is in', + nullable: true, + optional: true, + }, + surface: { + type: 'string', + description: 'Surface type of the venue', + nullable: true, + optional: true, + }, + national_team: { + type: 'boolean', + description: 'Whether the venue is used by the national team', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a State object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_STATE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the state' }, + state: { type: 'string', description: 'State code (e.g. NS, INPLAY_1ST_HALF)' }, + name: { type: 'string', description: 'Full name of the state (e.g. Not Started)' }, + short_name: { + type: 'string', + description: 'Short name of the state (e.g. NS)', + nullable: true, + optional: true, + }, + developer_name: { type: 'string', description: 'Developer name of the state', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Transfer object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_TRANSFER_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the transfer' }, + sport_id: { type: 'number', description: 'Sport of the transfer' }, + player_id: { type: 'number', description: 'Player who transferred' }, + type_id: { type: 'number', description: 'Type of the transfer' }, + from_team_id: { type: 'number', description: 'Team the player transferred from', nullable: true }, + to_team_id: { type: 'number', description: 'Team the player transferred to', nullable: true }, + position_id: { + type: 'number', + description: 'Position id of the transfer', + nullable: true, + optional: true, + }, + detailed_position_id: { + type: 'number', + description: 'Detailed position id of the transfer', + nullable: true, + optional: true, + }, + date: { type: 'string', description: 'Date of the transfer', nullable: true }, + career_ended: { + type: 'boolean', + description: 'Whether the transfer ended the career', + optional: true, + }, + completed: { type: 'boolean', description: 'Whether the transfer is completed', optional: true }, + amount: { type: 'number', description: 'Transfer fee amount', nullable: true, optional: true }, +} as const satisfies Record + +/** + * Output property definitions for an Expected (xG) by-team value. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/expected + */ +export const SPORTMONKS_EXPECTED_TEAM_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the expected value' }, + fixture_id: { type: 'number', description: 'Fixture related to the value' }, + type_id: { type: 'number', description: 'Type of the expected value' }, + participant_id: { type: 'number', description: 'Team related to the expected value' }, + data: { + type: 'object', + description: 'The expected value payload', + properties: { value: { type: 'number', description: 'The xG value' } }, + }, + location: { type: 'string', description: 'Home or away', nullable: true, optional: true }, +} as const satisfies Record + +/** Output property definitions for an Expected (xG) by-player value. */ +export const SPORTMONKS_EXPECTED_PLAYER_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the expected value' }, + fixture_id: { type: 'number', description: 'Fixture related to the value' }, + player_id: { type: 'number', description: 'Player related to the value' }, + team_id: { + type: 'number', + description: 'Team related to the value', + nullable: true, + optional: true, + }, + lineup_id: { + type: 'number', + description: 'Lineup record the player relates to', + nullable: true, + optional: true, + }, + type_id: { type: 'number', description: 'Type of the expected value' }, + data: { + type: 'object', + description: 'The expected value payload', + properties: { value: { type: 'number', description: 'The xG value' } }, + }, +} as const satisfies Record + +/** + * Output property definitions for a Prediction / Value Bet object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/odd-and-prediction + */ +export const SPORTMONKS_PREDICTION_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the prediction' }, + fixture_id: { type: 'number', description: 'Fixture related to the prediction' }, + predictions: { + type: 'json', + description: 'Prediction payload (varies by type: score map, value bet object, etc.)', + }, + type_id: { type: 'number', description: 'Type of the prediction' }, +} as const satisfies Record + +/** + * Output property definitions for a Commentary object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_COMMENTARY_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the commentary' }, + fixture_id: { type: 'number', description: 'Fixture related to the commentary' }, + comment: { type: 'string', description: 'The commentary text' }, + minute: { + type: 'number', + description: 'Match minute of the comment', + nullable: true, + optional: true, + }, + extra_minute: { + type: 'number', + description: 'Extra (injury) minute of the comment', + nullable: true, + optional: true, + }, + is_goal: { type: 'boolean', description: 'Whether the comment is a goal', optional: true }, + is_important: { + type: 'boolean', + description: 'Whether the comment is important', + optional: true, + }, + order: { type: 'number', description: 'Order of the comment', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a TV Station object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_TVSTATION_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the TV station' }, + name: { type: 'string', description: 'Name of the TV station' }, + url: { type: 'string', description: 'URL of the TV station', nullable: true, optional: true }, + image_path: { + type: 'string', + description: 'Image path of the TV station', + nullable: true, + optional: true, + }, + type: { type: 'string', description: 'Type of the TV station (tv, channel)', optional: true }, + related_id: { + type: 'number', + description: 'Related id of the TV station', + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Rival object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_RIVAL_PROPERTIES = { + sport_id: { type: 'number', description: 'Sport of the rival' }, + team_id: { type: 'number', description: 'Team the rivalry belongs to' }, + rival_id: { type: 'number', description: 'Rival team id' }, +} as const satisfies Record + +export interface SportmonksSeason { + id: number + sport_id: number + league_id: number + tie_breaker_rule_id?: number | null + name: string + finished?: boolean + pending?: boolean + is_current?: boolean + standing_method?: string | null + starting_at?: string | null + ending_at?: string | null + standings_recalculated_at?: string | null + games_in_current_week?: boolean +} + +export interface SportmonksStage { + id: number + sport_id: number + league_id: number + season_id: number + type_id: number + name: string + sort_order?: number + finished?: boolean + is_current?: boolean + starting_at?: string | null + ending_at?: string | null + games_in_current_week?: boolean +} + +export interface SportmonksRound { + id: number + sport_id: number + league_id: number + season_id: number + stage_id?: number | null + name: string + finished?: boolean + is_current?: boolean + starting_at?: string | null + ending_at?: string | null + games_in_current_week?: boolean +} + +export interface SportmonksCoach { + id: number + player_id?: number | null + sport_id: number + country_id: number | null + nationality_id: number | null + city_id?: number | null + common_name?: string + firstname?: string | null + lastname?: string | null + name: string + display_name?: string + image_path?: string + height?: number | null + weight?: number | null + date_of_birth?: string | null + gender?: string | null +} + +export interface SportmonksReferee { + id: number + sport_id: number + country_id: number | null + nationality_id?: number | null + city_id?: number | null + common_name?: string + firstname?: string | null + lastname?: string | null + name: string + display_name?: string + image_path?: string + height?: number | null + weight?: number | null + date_of_birth?: string | null + gender?: string | null +} + +export interface SportmonksVenue { + id: number + country_id: number | null + city_id?: number | null + name: string + address?: string | null + zipcode?: string | null + latitude?: string | null + longitude?: string | null + capacity?: number | null + image_path?: string | null + city_name?: string | null + surface?: string | null + national_team?: boolean +} + +export interface SportmonksState { + id: number + state: string + name: string + short_name?: string | null + developer_name?: string +} + +export interface SportmonksTransfer { + id: number + sport_id: number + player_id: number + type_id: number + from_team_id: number | null + to_team_id: number | null + position_id?: number | null + detailed_position_id?: number | null + date: string | null + career_ended?: boolean + completed?: boolean + amount?: number | null +} + +export interface SportmonksExpectedTeam { + id: number + fixture_id: number + type_id: number + participant_id: number + data: { value: number } + location?: string | null +} + +export interface SportmonksExpectedPlayer { + id: number + fixture_id: number + player_id: number + team_id?: number | null + lineup_id?: number | null + type_id: number + data: { value: number } +} + +export interface SportmonksPrediction { + id: number + fixture_id: number + predictions: Record + type_id: number +} + +export interface SportmonksCommentary { + id: number + fixture_id: number + comment: string + minute?: number | null + extra_minute?: number | null + is_goal?: boolean + is_important?: boolean + order?: number +} + +export interface SportmonksTVStation { + id: number + name: string + url?: string | null + image_path?: string | null + type?: string + related_id?: number | null +} + +export interface SportmonksRival { + sport_id: number + team_id: number + rival_id: number +} + +/** + * Output property definitions for a News (article) object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/other + */ +export const SPORTMONKS_NEWS_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the news article' }, + fixture_id: { type: 'number', description: 'Fixture related to the news article' }, + league_id: { type: 'number', description: 'League related to the news article' }, + title: { type: 'string', description: 'Title of the news article' }, + type: { type: 'string', description: 'Type of the news (prematch or postmatch)' }, +} as const satisfies Record + +/** + * Output property definitions for a Statistic object (season/stage/round). + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/statistic + */ +export const SPORTMONKS_STATISTIC_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the statistic record' }, + model_id: { type: 'number', description: 'Id of the entity the statistic belongs to' }, + type_id: { type: 'number', description: 'Type of the statistic' }, + relation_id: { + type: 'number', + description: 'Related entity id (e.g. participant) when applicable', + nullable: true, + optional: true, + }, + value: { type: 'json', description: 'Statistic value payload (varies by type)' }, +} as const satisfies Record + +/** + * Output property definitions for a Standing Correction object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/standing-and-topscorer + */ +export const SPORTMONKS_STANDING_CORRECTION_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the standing correction' }, + season_id: { type: 'number', description: 'Season related to the correction' }, + stage_id: { type: 'number', description: 'Stage related to the correction', nullable: true }, + group_id: { type: 'number', description: 'Group related to the correction', nullable: true }, + type_id: { type: 'number', description: 'Type of the correction' }, + value: { type: 'number', description: 'Amount of points awarded or deducted' }, + calc_type: { type: 'string', description: 'Calculation type applied (e.g. + or -)' }, + participant_type: { type: 'string', description: 'Type of the participant (e.g. team)' }, + participant_id: { type: 'number', description: 'Participant the correction applies to' }, + active: { type: 'boolean', description: 'Whether the correction is active', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Match Fact object (beta). + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/match-facts-beta + */ +export const SPORTMONKS_MATCH_FACT_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the match fact' }, + sport_id: { type: 'number', description: 'Sport of the match fact' }, + fixture_id: { type: 'number', description: 'Fixture related to the match fact' }, + type_id: { type: 'number', description: 'Type of the match fact' }, + participant: { type: 'string', description: 'Team the fact relates to (home or away)' }, + basis: { type: 'string', description: 'Basis of the match fact (e.g. h2h, overall)' }, + data: { type: 'json', description: 'Match fact data payload (counts and percentages)' }, + natural_language: { + type: 'string', + description: 'Human-readable description of the match fact', + optional: true, + }, + category: { type: 'string', description: 'Category of the match fact', optional: true }, + scope: { type: 'string', description: 'Scope of the match fact', optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Team Ranking object (beta). + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/team-rankings-beta + */ +export const SPORTMONKS_TEAM_RANKING_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the team ranking' }, + team_id: { type: 'number', description: 'Team related to the ranking' }, + date: { type: 'string', description: 'Date of the ranking' }, + current_rank: { type: 'number', description: 'Placement of the team on that date' }, + scaled_score: { type: 'number', description: 'Scaled score of the team (0-100)' }, +} as const satisfies Record + +/** + * Output property definitions for a Team of the Week (TOTW) entry. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/team-of-the-week-totw + */ +export const SPORTMONKS_TOTW_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the TOTW entry' }, + player_id: { type: 'number', description: 'Player of the team of the week' }, + fixture_id: { type: 'number', description: 'Fixture the TOTW player played in' }, + round_id: { type: 'number', description: 'Round the fixture is played at' }, + team_id: { type: 'number', description: 'Team the TOTW player played for' }, + rating: { type: 'string', description: 'Rating of the TOTW player' }, + formation_position: { + type: 'number', + description: 'Player position in the TOTW formation', + optional: true, + }, + formation: { type: 'string', description: "The TOTW's formation", optional: true }, +} as const satisfies Record + +/** + * Output property definitions for a Predictability object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/entities/odd-and-prediction + */ +export const SPORTMONKS_PREDICTABILITY_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the predictability record' }, + league_id: { type: 'number', description: 'League related to the predictability' }, + type_id: { type: 'number', description: 'Type of the predictability' }, + data: { type: 'json', description: 'Predictability values per market' }, +} as const satisfies Record + +/** + * Output property definitions for a Live Probability object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/predictions/get-live-probabilities + */ +export const SPORTMONKS_LIVE_PROBABILITY_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the live prediction record' }, + fixture_id: { type: 'number', description: 'Fixture the prediction belongs to' }, + period_id: { type: 'number', description: 'Match period the prediction was recorded in' }, + minute: { type: 'number', description: 'Match minute the prediction was generated' }, + predictions: { + type: 'json', + description: 'Home win, away win and draw probabilities as percentages', + }, + type_id: { type: 'number', description: 'Type of the prediction (237 for fulltime result)' }, +} as const satisfies Record + +/** + * Output property definitions for a Transfer Rumour object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/transfer-rumours + */ +export const SPORTMONKS_TRANSFER_RUMOUR_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the transfer rumour' }, + sport_id: { type: 'number', description: 'Sport of the transfer rumour' }, + player_id: { type: 'number', description: 'Player the rumour relates to' }, + position_id: { + type: 'number', + description: 'Position id of the player', + nullable: true, + optional: true, + }, + from_team_id: { + type: 'number', + description: 'Team the player would transfer from', + nullable: true, + }, + to_team_id: { type: 'number', description: 'Team the player would transfer to', nullable: true }, + transfer_fee_id: { + type: 'number', + description: 'Transfer fee id of the rumour', + nullable: true, + optional: true, + }, + probability: { type: 'string', description: 'Probability of the rumour (e.g. LOW)' }, + source_name: { + type: 'string', + description: 'Name of the source of the rumour', + nullable: true, + optional: true, + }, + source_url: { + type: 'string', + description: 'URL of the source of the rumour', + nullable: true, + optional: true, + }, + amount: { type: 'number', description: 'Estimated transfer fee amount', nullable: true }, + currency: { + type: 'string', + description: 'Currency of the amount', + nullable: true, + optional: true, + }, + date: { type: 'string', description: 'Date of the rumour', nullable: true }, + type_id: { type: 'number', description: 'Type of the transfer rumour' }, +} as const satisfies Record + +/** + * Output property definitions for an Expected Lineup (premium) object. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/premium-expected-lineups + */ +export const SPORTMONKS_EXPECTED_LINEUP_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the expected lineup record' }, + sport_id: { type: 'number', description: 'Sport of the expected lineup' }, + fixture_id: { type: 'number', description: 'Fixture the expected lineup relates to' }, + player_id: { type: 'number', description: 'Player in the expected lineup' }, + team_id: { type: 'number', description: 'Team of the expected lineup player' }, + formation_field: { + type: 'string', + description: 'Formation field of the player', + nullable: true, + optional: true, + }, + position_id: { + type: 'number', + description: 'Position id of the player', + nullable: true, + optional: true, + }, + detailed_position_id: { + type: 'number', + description: 'Detailed position id of the player', + nullable: true, + optional: true, + }, + type_id: { type: 'number', description: 'Type of the expected lineup record' }, + formation_position: { + type: 'number', + description: 'Position of the player in the formation', + nullable: true, + optional: true, + }, + player_name: { type: 'string', description: 'Name of the player', optional: true }, + jersey_number: { + type: 'number', + description: 'Jersey number of the player', + nullable: true, + optional: true, + }, +} as const satisfies Record + +export interface SportmonksNews { + id: number + fixture_id: number + league_id: number + title: string + type: string +} + +export interface SportmonksStatistic { + id: number + model_id: number + type_id: number + relation_id?: number | null + value: Record +} + +export interface SportmonksStandingCorrection { + id: number + season_id: number + stage_id: number | null + group_id: number | null + type_id: number + value: number + calc_type: string + participant_type: string + participant_id: number + active?: boolean +} + +export interface SportmonksMatchFact { + id: number + sport_id: number + fixture_id: number + type_id: number + participant: string + basis: string + data: Record + natural_language?: string + category?: string + scope?: string +} + +export interface SportmonksTeamRanking { + id: number + team_id: number + date: string + current_rank: number + scaled_score: number +} + +export interface SportmonksTotw { + id: number + player_id: number + fixture_id: number + round_id: number + team_id: number + rating: string + formation_position?: number + formation?: string +} + +export interface SportmonksPredictability { + id: number + league_id: number + type_id: number + data: Record +} + +export interface SportmonksLiveProbability { + id: number + fixture_id: number + period_id: number + minute: number + predictions: Record + type_id: number +} + +export interface SportmonksTransferRumour { + id: number + sport_id: number + player_id: number + position_id?: number | null + from_team_id: number | null + to_team_id: number | null + transfer_fee_id?: number | null + probability: string + source_name?: string | null + source_url?: string | null + amount: number | null + currency?: string | null + date: string | null + type_id: number +} + +export interface SportmonksExpectedLineup { + id: number + sport_id: number + fixture_id: number + player_id: number + team_id: number + formation_field?: string | null + position_id?: number | null + detailed_position_id?: number | null + type_id: number + formation_position?: number | null + player_name?: string + jersey_number?: number | null +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_all_fixtures.ts b/apps/sim/tools/sportmonks_motorsport/get_all_fixtures.ts new file mode 100644 index 00000000000..741f0c8ad6c --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_all_fixtures.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetAllFixturesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetAllFixturesResponse extends ToolResponse { + output: { + fixtures: SportmonksMsFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetAllFixturesTool: ToolConfig< + SportmonksMsGetAllFixturesParams, + SportmonksMsGetAllFixturesResponse +> = { + id: 'sportmonks_motorsport_get_all_fixtures', + name: 'Get All Motorsport Fixtures', + description: 'Retrieve all motorsport fixtures (sessions) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;results)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_fixtures') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of motorsport fixture (session) objects', + items: { type: 'object', properties: SPORTMONKS_MS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_current_leagues_by_team.ts b/apps/sim/tools/sportmonks_motorsport/get_current_leagues_by_team.ts new file mode 100644 index 00000000000..691f480dc82 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_current_leagues_by_team.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetCurrentLeaguesByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksMsGetCurrentLeaguesByTeamResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetCurrentLeaguesByTeamTool: ToolConfig< + SportmonksMsGetCurrentLeaguesByTeamParams, + SportmonksMsGetCurrentLeaguesByTeamResponse +> = { + id: 'sportmonks_motorsport_get_current_leagues_by_team', + name: 'Get Current Leagues by Team', + description: 'Retrieve the current motorsport leagues for a team by team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team (constructor)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/teams/${encodeURIComponent(params.teamId.trim())}/current` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_current_leagues_by_team') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of current league objects for the team', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_driver_standings.ts b/apps/sim/tools/sportmonks_motorsport/get_driver_standings.ts new file mode 100644 index 00000000000..5c2ac3716e4 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_driver_standings.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STANDING_PROPERTIES, + type SportmonksMsStanding, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetDriverStandingsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetDriverStandingsResponse extends ToolResponse { + output: { + standings: SportmonksMsStanding[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetDriverStandingsTool: ToolConfig< + SportmonksMsGetDriverStandingsParams, + SportmonksMsGetDriverStandingsResponse +> = { + id: 'sportmonks_motorsport_get_driver_standings', + name: 'Get All Driver Standings', + description: 'Retrieve all driver championship standings from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/standings/drivers`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_driver_standings') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of driver standing entries', + items: { type: 'object', properties: SPORTMONKS_MS_STANDING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_drivers_by_country.ts b/apps/sim/tools/sportmonks_motorsport/get_drivers_by_country.ts new file mode 100644 index 00000000000..24e6fc1a9bd --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_drivers_by_country.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_DRIVER_PROPERTIES, + type SportmonksMsDriver, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetDriversByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksMsGetDriversByCountryResponse extends ToolResponse { + output: { + drivers: SportmonksMsDriver[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetDriversByCountryTool: ToolConfig< + SportmonksMsGetDriversByCountryParams, + SportmonksMsGetDriversByCountryResponse +> = { + id: 'sportmonks_motorsport_get_drivers_by_country', + name: 'Get Drivers by Country', + description: 'Retrieve all motorsport drivers for a country by country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/drivers/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_drivers_by_country') + } + return { + success: true, + output: { + drivers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + drivers: { + type: 'array', + description: 'Array of driver objects for the country', + items: { type: 'object', properties: SPORTMONKS_MS_DRIVER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_drivers_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_drivers_by_season.ts new file mode 100644 index 00000000000..cc977dc7bb0 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_drivers_by_season.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_DRIVER_PROPERTIES, + type SportmonksMsDriver, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetDriversBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetDriversBySeasonResponse extends ToolResponse { + output: { + drivers: SportmonksMsDriver[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetDriversBySeasonTool: ToolConfig< + SportmonksMsGetDriversBySeasonParams, + SportmonksMsGetDriversBySeasonResponse +> = { + id: 'sportmonks_motorsport_get_drivers_by_season', + name: 'Get Drivers by Season', + description: 'Retrieve all motorsport drivers for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/drivers/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_drivers_by_season') + } + return { + success: true, + output: { + drivers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + drivers: { + type: 'array', + description: 'Array of driver objects for the season', + items: { type: 'object', properties: SPORTMONKS_MS_DRIVER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date_range.ts b/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date_range.ts new file mode 100644 index 00000000000..62cd2b7411b --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_date_range.ts @@ -0,0 +1,123 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetFixturesByDateRangeParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + startDate: string + endDate: string +} + +export interface SportmonksMsGetFixturesByDateRangeResponse extends ToolResponse { + output: { + fixtures: SportmonksMsFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetFixturesByDateRangeTool: ToolConfig< + SportmonksMsGetFixturesByDateRangeParams, + SportmonksMsGetFixturesByDateRangeResponse +> = { + id: 'sportmonks_motorsport_get_fixtures_by_date_range', + name: 'Get Motorsport Fixtures by Date Range', + description: + 'Retrieve motorsport fixtures (sessions) between two dates (YYYY-MM-DD, max 100 days) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + startDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The start date of the range, in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The end date of the range, in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participants;venue)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order fixtures by starting_at (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/between/${encodeURIComponent(params.startDate.trim())}/${encodeURIComponent(params.endDate.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_date_range') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of motorsport fixture (session) objects within the requested date range', + items: { type: 'object', properties: SPORTMONKS_MS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_ids.ts b/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_ids.ts new file mode 100644 index 00000000000..fc62a4da3b6 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_fixtures_by_ids.ts @@ -0,0 +1,117 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetFixturesByIdsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureIds: string +} + +export interface SportmonksMsGetFixturesByIdsResponse extends ToolResponse { + output: { + fixtures: SportmonksMsFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetFixturesByIdsTool: ToolConfig< + SportmonksMsGetFixturesByIdsParams, + SportmonksMsGetFixturesByIdsResponse +> = { + id: 'sportmonks_motorsport_get_fixtures_by_ids', + name: 'Get Motorsport Fixtures by IDs', + description: + 'Retrieve multiple motorsport fixtures (sessions) by their IDs (max 50) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated list of fixture ids (max 50, e.g. 19408487,19408480)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;results)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/multi/${encodeURIComponent(params.fixtureIds.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_fixtures_by_ids') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of motorsport fixture (session) objects for the requested ids', + items: { type: 'object', properties: SPORTMONKS_MS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_driver.ts b/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_driver.ts new file mode 100644 index 00000000000..7d2c6d5a200 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_driver.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLapsByFixtureAndDriverParams extends SportmonksBaseParams { + fixtureId: string + driverId: string +} + +export interface SportmonksMsGetLapsByFixtureAndDriverResponse extends ToolResponse { + output: { + laps: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetLapsByFixtureAndDriverTool: ToolConfig< + SportmonksMsGetLapsByFixtureAndDriverParams, + SportmonksMsGetLapsByFixtureAndDriverResponse +> = { + id: 'sportmonks_motorsport_get_laps_by_fixture_and_driver', + name: 'Get Laps by Fixture and Driver', + description: 'Retrieve all laps for a motorsport fixture and driver from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + driverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the driver', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/laps/drivers/${encodeURIComponent(params.driverId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_laps_by_fixture_and_driver') + } + return { + success: true, + output: { + laps: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + laps: { + type: 'array', + description: 'Array of lap objects for the fixture and driver', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_lap.ts b/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_lap.ts new file mode 100644 index 00000000000..54998fa692b --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_laps_by_fixture_and_lap.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLapsByFixtureAndLapParams extends SportmonksBaseParams { + fixtureId: string + lapNumber: string +} + +export interface SportmonksMsGetLapsByFixtureAndLapResponse extends ToolResponse { + output: { + laps: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetLapsByFixtureAndLapTool: ToolConfig< + SportmonksMsGetLapsByFixtureAndLapParams, + SportmonksMsGetLapsByFixtureAndLapResponse +> = { + id: 'sportmonks_motorsport_get_laps_by_fixture_and_lap', + name: 'Get Laps by Fixture and Lap Number', + description: 'Retrieve all laps for a motorsport fixture and lap number from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + lapNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The lap number to retrieve', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/laps/${encodeURIComponent(params.lapNumber.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_laps_by_fixture_and_lap') + } + return { + success: true, + output: { + laps: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + laps: { + type: 'array', + description: 'Array of lap objects for the fixture and lap number', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_latest_laps_by_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_latest_laps_by_fixture.ts new file mode 100644 index 00000000000..0d24a7943e0 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_latest_laps_by_fixture.ts @@ -0,0 +1,91 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLatestLapsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetLatestLapsByFixtureResponse extends ToolResponse { + output: { + laps: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetLatestLapsByFixtureTool: ToolConfig< + SportmonksMsGetLatestLapsByFixtureParams, + SportmonksMsGetLatestLapsByFixtureResponse +> = { + id: 'sportmonks_motorsport_get_latest_laps_by_fixture', + name: 'Get Latest Laps by Fixture', + description: + 'Retrieve the latest laps for a motorsport fixture (session) by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/laps/latest` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_laps_by_fixture') + } + return { + success: true, + output: { + laps: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + laps: { + type: 'array', + description: 'Array of the latest lap objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_latest_pitstops_by_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_latest_pitstops_by_fixture.ts new file mode 100644 index 00000000000..711576875de --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_latest_pitstops_by_fixture.ts @@ -0,0 +1,91 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLatestPitstopsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetLatestPitstopsByFixtureResponse extends ToolResponse { + output: { + pitstops: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetLatestPitstopsByFixtureTool: ToolConfig< + SportmonksMsGetLatestPitstopsByFixtureParams, + SportmonksMsGetLatestPitstopsByFixtureResponse +> = { + id: 'sportmonks_motorsport_get_latest_pitstops_by_fixture', + name: 'Get Latest Pitstops by Fixture', + description: + 'Retrieve the latest pitstops for a motorsport fixture (session) by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/pitstops/latest` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_pitstops_by_fixture') + } + return { + success: true, + output: { + pitstops: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + pitstops: { + type: 'array', + description: 'Array of the latest pitstop objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_latest_stints_by_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_latest_stints_by_fixture.ts new file mode 100644 index 00000000000..44ce3a37bfa --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_latest_stints_by_fixture.ts @@ -0,0 +1,91 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STINT_PROPERTIES, + type SportmonksMsStint, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLatestStintsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetLatestStintsByFixtureResponse extends ToolResponse { + output: { + stints: SportmonksMsStint[] + } +} + +export const sportmonksMotorsportGetLatestStintsByFixtureTool: ToolConfig< + SportmonksMsGetLatestStintsByFixtureParams, + SportmonksMsGetLatestStintsByFixtureResponse +> = { + id: 'sportmonks_motorsport_get_latest_stints_by_fixture', + name: 'Get Latest Stints by Fixture', + description: + 'Retrieve the latest tyre stints for a motorsport fixture (session) by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/stints/latest` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_stints_by_fixture') + } + return { + success: true, + output: { + stints: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + stints: { + type: 'array', + description: 'Array of the latest stint objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MS_STINT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_latest_updated_drivers.ts b/apps/sim/tools/sportmonks_motorsport/get_latest_updated_drivers.ts new file mode 100644 index 00000000000..e357839e842 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_latest_updated_drivers.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_DRIVER_PROPERTIES, + type SportmonksMsDriver, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLatestUpdatedDriversParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetLatestUpdatedDriversResponse extends ToolResponse { + output: { + drivers: SportmonksMsDriver[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLatestUpdatedDriversTool: ToolConfig< + SportmonksMsGetLatestUpdatedDriversParams, + SportmonksMsGetLatestUpdatedDriversResponse +> = { + id: 'sportmonks_motorsport_get_latest_updated_drivers', + name: 'Get Latest Updated Drivers', + description: 'Retrieve the most recently updated motorsport drivers from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/drivers/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_updated_drivers') + } + return { + success: true, + output: { + drivers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + drivers: { + type: 'array', + description: 'Array of recently updated driver objects', + items: { type: 'object', properties: SPORTMONKS_MS_DRIVER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_latest_updated_fixtures.ts b/apps/sim/tools/sportmonks_motorsport/get_latest_updated_fixtures.ts new file mode 100644 index 00000000000..f4753f63835 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_latest_updated_fixtures.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_FIXTURE_PROPERTIES, + type SportmonksMsFixture, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLatestUpdatedFixturesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetLatestUpdatedFixturesResponse extends ToolResponse { + output: { + fixtures: SportmonksMsFixture[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLatestUpdatedFixturesTool: ToolConfig< + SportmonksMsGetLatestUpdatedFixturesParams, + SportmonksMsGetLatestUpdatedFixturesResponse +> = { + id: 'sportmonks_motorsport_get_latest_updated_fixtures', + name: 'Get Latest Updated Motorsport Fixtures', + description: 'Retrieve the most recently updated motorsport fixtures (sessions) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participants;results)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_latest_updated_fixtures') + } + return { + success: true, + output: { + fixtures: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + fixtures: { + type: 'array', + description: 'Array of recently updated motorsport fixture (session) objects', + items: { type: 'object', properties: SPORTMONKS_MS_FIXTURE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_league.ts b/apps/sim/tools/sportmonks_motorsport/get_league.ts new file mode 100644 index 00000000000..154102f9a60 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_league.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLeagueParams extends SportmonksBaseParams { + leagueId: string +} + +export interface SportmonksMsGetLeagueResponse extends ToolResponse { + output: { + league: SportmonksMsLeague | null + } +} + +export const sportmonksMotorsportGetLeagueTool: ToolConfig< + SportmonksMsGetLeagueParams, + SportmonksMsGetLeagueResponse +> = { + id: 'sportmonks_motorsport_get_league', + name: 'Get League by ID', + description: 'Retrieve a single motorsport league by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + leagueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the league', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/${encodeURIComponent(params.leagueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_league') + } + return { + success: true, + output: { + league: data.data ?? null, + }, + } + }, + + outputs: { + league: { + type: 'object', + description: 'The requested league object', + properties: SPORTMONKS_MS_LEAGUE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_leagues.ts b/apps/sim/tools/sportmonks_motorsport/get_leagues.ts new file mode 100644 index 00000000000..864566b3845 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_leagues.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLeaguesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetLeaguesResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLeaguesTool: ToolConfig< + SportmonksMsGetLeaguesParams, + SportmonksMsGetLeaguesResponse +> = { + id: 'sportmonks_motorsport_get_leagues', + name: 'Get All Leagues', + description: 'Retrieve all motorsport leagues from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_leagues_by_country.ts b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_country.ts new file mode 100644 index 00000000000..29fba8e8470 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_country.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLeaguesByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksMsGetLeaguesByCountryResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLeaguesByCountryTool: ToolConfig< + SportmonksMsGetLeaguesByCountryParams, + SportmonksMsGetLeaguesByCountryResponse +> = { + id: 'sportmonks_motorsport_get_leagues_by_country', + name: 'Get Leagues by Country', + description: 'Retrieve all motorsport leagues for a country by country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_country') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects for the country', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_leagues_by_date.ts b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_date.ts new file mode 100644 index 00000000000..024bc334722 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_date.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLeaguesByDateParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + date: string +} + +export interface SportmonksMsGetLeaguesByDateResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLeaguesByDateTool: ToolConfig< + SportmonksMsGetLeaguesByDateParams, + SportmonksMsGetLeaguesByDateResponse +> = { + id: 'sportmonks_motorsport_get_leagues_by_date', + name: 'Get Leagues by Fixture Date', + description: + 'Retrieve all motorsport leagues with fixtures on a specific date (YYYY-MM-DD) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + date: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The date to fetch leagues for, in YYYY-MM-DD format', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/date/${encodeURIComponent(params.date.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_date') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects with fixtures on the requested date', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_leagues_by_live.ts b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_live.ts new file mode 100644 index 00000000000..6cedc2f847b --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_live.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLeaguesByLiveParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetLeaguesByLiveResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLeaguesByLiveTool: ToolConfig< + SportmonksMsGetLeaguesByLiveParams, + SportmonksMsGetLeaguesByLiveResponse +> = { + id: 'sportmonks_motorsport_get_leagues_by_live', + name: 'Get Leagues by Live', + description: 'Retrieve all motorsport leagues that currently have live fixtures from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/live`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_live') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects that currently have live fixtures', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_leagues_by_team.ts b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_team.ts new file mode 100644 index 00000000000..f4f5b6085a9 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_leagues_by_team.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetLeaguesByTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + teamId: string +} + +export interface SportmonksMsGetLeaguesByTeamResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetLeaguesByTeamTool: ToolConfig< + SportmonksMsGetLeaguesByTeamParams, + SportmonksMsGetLeaguesByTeamResponse +> = { + id: 'sportmonks_motorsport_get_leagues_by_team', + name: 'Get Leagues by Team', + description: + 'Retrieve all current and historical motorsport leagues for a team by team ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team (constructor)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_leagues_by_team') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects for the team', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_driver.ts b/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_driver.ts new file mode 100644 index 00000000000..15326780a68 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_driver.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetPitstopsByFixtureAndDriverParams extends SportmonksBaseParams { + fixtureId: string + driverId: string +} + +export interface SportmonksMsGetPitstopsByFixtureAndDriverResponse extends ToolResponse { + output: { + pitstops: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetPitstopsByFixtureAndDriverTool: ToolConfig< + SportmonksMsGetPitstopsByFixtureAndDriverParams, + SportmonksMsGetPitstopsByFixtureAndDriverResponse +> = { + id: 'sportmonks_motorsport_get_pitstops_by_fixture_and_driver', + name: 'Get Pitstops by Fixture and Driver', + description: 'Retrieve all pitstops for a motorsport fixture and driver from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + driverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the driver', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/pitstops/drivers/${encodeURIComponent(params.driverId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_pitstops_by_fixture_and_driver') + } + return { + success: true, + output: { + pitstops: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + pitstops: { + type: 'array', + description: 'Array of pitstop objects for the fixture and driver', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_lap.ts b/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_lap.ts new file mode 100644 index 00000000000..9e74c40983c --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_pitstops_by_fixture_and_lap.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LAP_PROPERTIES, + type SportmonksMsLap, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetPitstopsByFixtureAndLapParams extends SportmonksBaseParams { + fixtureId: string + lapNumber: string +} + +export interface SportmonksMsGetPitstopsByFixtureAndLapResponse extends ToolResponse { + output: { + pitstops: SportmonksMsLap[] + } +} + +export const sportmonksMotorsportGetPitstopsByFixtureAndLapTool: ToolConfig< + SportmonksMsGetPitstopsByFixtureAndLapParams, + SportmonksMsGetPitstopsByFixtureAndLapResponse +> = { + id: 'sportmonks_motorsport_get_pitstops_by_fixture_and_lap', + name: 'Get Pitstops by Fixture and Lap Number', + description: 'Retrieve all pitstops for a motorsport fixture and lap number from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + lapNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The lap number to retrieve', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/pitstops/${encodeURIComponent(params.lapNumber.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_pitstops_by_fixture_and_lap') + } + return { + success: true, + output: { + pitstops: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + pitstops: { + type: 'array', + description: 'Array of pitstop objects for the fixture and lap number', + items: { type: 'object', properties: SPORTMONKS_MS_LAP_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_driver.ts b/apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_driver.ts new file mode 100644 index 00000000000..3fcbd696bb2 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_driver.ts @@ -0,0 +1,124 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetRaceResultsBySeasonAndDriverParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string + driverId: string +} + +export interface SportmonksMsGetRaceResultsBySeasonAndDriverResponse extends ToolResponse { + output: { + results: SportmonksMsStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetRaceResultsBySeasonAndDriverTool: ToolConfig< + SportmonksMsGetRaceResultsBySeasonAndDriverParams, + SportmonksMsGetRaceResultsBySeasonAndDriverResponse +> = { + id: 'sportmonks_motorsport_get_race_results_by_season_and_driver', + name: 'Get Race Results by Season and Driver', + description: + 'Retrieve race results (stages with fixtures, lineups and lineup details) for a season and driver from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + driverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the driver', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/results/seasons/${encodeURIComponent(params.seasonId.trim())}/drivers/${encodeURIComponent(params.driverId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_race_results_by_season_and_driver') + } + return { + success: true, + output: { + results: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + results: { + type: 'array', + description: + 'Array of stage objects for the season and driver, each including nested fixtures, lineups and lineup details', + items: { type: 'object', properties: SPORTMONKS_MS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_team.ts b/apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_team.ts new file mode 100644 index 00000000000..ddcf3082df3 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_race_results_by_season_and_team.ts @@ -0,0 +1,124 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetRaceResultsBySeasonAndTeamParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string + teamId: string +} + +export interface SportmonksMsGetRaceResultsBySeasonAndTeamResponse extends ToolResponse { + output: { + results: SportmonksMsStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetRaceResultsBySeasonAndTeamTool: ToolConfig< + SportmonksMsGetRaceResultsBySeasonAndTeamParams, + SportmonksMsGetRaceResultsBySeasonAndTeamResponse +> = { + id: 'sportmonks_motorsport_get_race_results_by_season_and_team', + name: 'Get Race Results by Season and Team', + description: + 'Retrieve race results (stages with fixtures, lineups and lineup details) for a season and team from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the team (constructor)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/results/seasons/${encodeURIComponent(params.seasonId.trim())}/teams/${encodeURIComponent(params.teamId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_race_results_by_season_and_team') + } + return { + success: true, + output: { + results: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + results: { + type: 'array', + description: + 'Array of stage objects for the season and team, each including nested fixtures, lineups and lineup details', + items: { type: 'object', properties: SPORTMONKS_MS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_schedules_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_schedules_by_season.ts new file mode 100644 index 00000000000..182ce0090d9 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_schedules_by_season.ts @@ -0,0 +1,117 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetSchedulesBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetSchedulesBySeasonResponse extends ToolResponse { + output: { + schedules: SportmonksMsStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetSchedulesBySeasonTool: ToolConfig< + SportmonksMsGetSchedulesBySeasonParams, + SportmonksMsGetSchedulesBySeasonResponse +> = { + id: 'sportmonks_motorsport_get_schedules_by_season', + name: 'Get Schedules by Season', + description: + 'Retrieve the full schedule (stages with nested fixtures and venues) for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/schedules/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_schedules_by_season') + } + return { + success: true, + output: { + schedules: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + schedules: { + type: 'array', + description: + 'Array of stage objects for the season schedule, each including nested fixtures and venues', + items: { type: 'object', properties: SPORTMONKS_MS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_season.ts b/apps/sim/tools/sportmonks_motorsport/get_season.ts new file mode 100644 index 00000000000..6019601d400 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_season.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_SEASON_PROPERTIES, + type SportmonksMsSeason, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetSeasonParams extends SportmonksBaseParams { + seasonId: string +} + +export interface SportmonksMsGetSeasonResponse extends ToolResponse { + output: { + season: SportmonksMsSeason | null + } +} + +export const sportmonksMotorsportGetSeasonTool: ToolConfig< + SportmonksMsGetSeasonParams, + SportmonksMsGetSeasonResponse +> = { + id: 'sportmonks_motorsport_get_season', + name: 'Get Season by ID', + description: 'Retrieve a single motorsport season by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;stages)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_season') + } + return { + success: true, + output: { + season: data.data ?? null, + }, + } + }, + + outputs: { + season: { + type: 'object', + description: 'The requested season object', + properties: SPORTMONKS_MS_SEASON_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_seasons.ts b/apps/sim/tools/sportmonks_motorsport/get_seasons.ts new file mode 100644 index 00000000000..ce2891307ef --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_seasons.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_SEASON_PROPERTIES, + type SportmonksMsSeason, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetSeasonsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetSeasonsResponse extends ToolResponse { + output: { + seasons: SportmonksMsSeason[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetSeasonsTool: ToolConfig< + SportmonksMsGetSeasonsParams, + SportmonksMsGetSeasonsResponse +> = { + id: 'sportmonks_motorsport_get_seasons', + name: 'Get All Seasons', + description: 'Retrieve all motorsport seasons from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. league;stages)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/seasons`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_seasons') + } + return { + success: true, + output: { + seasons: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + seasons: { + type: 'array', + description: 'Array of season objects', + items: { type: 'object', properties: SPORTMONKS_MS_SEASON_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_stage.ts b/apps/sim/tools/sportmonks_motorsport/get_stage.ts new file mode 100644 index 00000000000..026d2237251 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_stage.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStageParams extends SportmonksBaseParams { + stageId: string +} + +export interface SportmonksMsGetStageResponse extends ToolResponse { + output: { + stage: SportmonksMsStage | null + } +} + +export const sportmonksMotorsportGetStageTool: ToolConfig< + SportmonksMsGetStageParams, + SportmonksMsGetStageResponse +> = { + id: 'sportmonks_motorsport_get_stage', + name: 'Get Stage by ID', + description: 'Retrieve a single motorsport stage (race weekend) by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + stageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the stage (race weekend)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/stages/${encodeURIComponent(params.stageId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stage') + } + return { + success: true, + output: { + stage: data.data ?? null, + }, + } + }, + + outputs: { + stage: { + type: 'object', + description: 'The requested stage (race weekend) object', + properties: SPORTMONKS_MS_STAGE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_stages.ts b/apps/sim/tools/sportmonks_motorsport/get_stages.ts new file mode 100644 index 00000000000..45345827aa5 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_stages.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStagesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetStagesResponse extends ToolResponse { + output: { + stages: SportmonksMsStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetStagesTool: ToolConfig< + SportmonksMsGetStagesParams, + SportmonksMsGetStagesResponse +> = { + id: 'sportmonks_motorsport_get_stages', + name: 'Get All Stages', + description: 'Retrieve all motorsport stages (race weekends) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/stages`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stages') + } + return { + success: true, + output: { + stages: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + stages: { + type: 'array', + description: 'Array of stage (race weekend) objects', + items: { type: 'object', properties: SPORTMONKS_MS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_stages_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_stages_by_season.ts new file mode 100644 index 00000000000..4cc08fd1df0 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_stages_by_season.ts @@ -0,0 +1,117 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStagesBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetStagesBySeasonResponse extends ToolResponse { + output: { + stages: SportmonksMsStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetStagesBySeasonTool: ToolConfig< + SportmonksMsGetStagesBySeasonParams, + SportmonksMsGetStagesBySeasonResponse +> = { + id: 'sportmonks_motorsport_get_stages_by_season', + name: 'Get Stages by Season', + description: + 'Retrieve all motorsport stages (race weekends) for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/stages/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stages_by_season') + } + return { + success: true, + output: { + stages: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + stages: { + type: 'array', + description: 'Array of stage (race weekend) objects for the season', + items: { type: 'object', properties: SPORTMONKS_MS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_state.ts b/apps/sim/tools/sportmonks_motorsport/get_state.ts new file mode 100644 index 00000000000..2ba8236a08b --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_state.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STATE_PROPERTIES, + type SportmonksMsState, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStateParams extends SportmonksBaseParams { + stateId: string +} + +export interface SportmonksMsGetStateResponse extends ToolResponse { + output: { + state: SportmonksMsState | null + } +} + +export const sportmonksMotorsportGetStateTool: ToolConfig< + SportmonksMsGetStateParams, + SportmonksMsGetStateResponse +> = { + id: 'sportmonks_motorsport_get_state', + name: 'Get State by ID', + description: 'Retrieve a single motorsport fixture state by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + stateId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the state', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/states/${encodeURIComponent(params.stateId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_state') + } + return { + success: true, + output: { + state: data.data ?? null, + }, + } + }, + + outputs: { + state: { + type: 'object', + description: 'The requested fixture state object', + properties: SPORTMONKS_MS_STATE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_states.ts b/apps/sim/tools/sportmonks_motorsport/get_states.ts new file mode 100644 index 00000000000..744f2ca48a9 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_states.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STATE_PROPERTIES, + type SportmonksMsState, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStatesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetStatesResponse extends ToolResponse { + output: { + states: SportmonksMsState[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetStatesTool: ToolConfig< + SportmonksMsGetStatesParams, + SportmonksMsGetStatesResponse +> = { + id: 'sportmonks_motorsport_get_states', + name: 'Get All States', + description: 'Retrieve all possible motorsport fixture states from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/states`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_states') + } + return { + success: true, + output: { + states: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + states: { + type: 'array', + description: 'Array of fixture state objects', + items: { type: 'object', properties: SPORTMONKS_MS_STATE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture.ts b/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture.ts new file mode 100644 index 00000000000..5696ae4672e --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture.ts @@ -0,0 +1,91 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STINT_PROPERTIES, + type SportmonksMsStint, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStintsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksMsGetStintsByFixtureResponse extends ToolResponse { + output: { + stints: SportmonksMsStint[] + } +} + +export const sportmonksMotorsportGetStintsByFixtureTool: ToolConfig< + SportmonksMsGetStintsByFixtureParams, + SportmonksMsGetStintsByFixtureResponse +> = { + id: 'sportmonks_motorsport_get_stints_by_fixture', + name: 'Get Stints by Fixture', + description: + 'Retrieve all tyre stints for a motorsport fixture (session) by fixture ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/stints` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stints_by_fixture') + } + return { + success: true, + output: { + stints: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + stints: { + type: 'array', + description: 'Array of stint objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_MS_STINT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_driver.ts b/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_driver.ts new file mode 100644 index 00000000000..43623b3ebf9 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_driver.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STINT_PROPERTIES, + type SportmonksMsStint, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStintsByFixtureAndDriverParams extends SportmonksBaseParams { + fixtureId: string + driverId: string +} + +export interface SportmonksMsGetStintsByFixtureAndDriverResponse extends ToolResponse { + output: { + stints: SportmonksMsStint[] + } +} + +export const sportmonksMotorsportGetStintsByFixtureAndDriverTool: ToolConfig< + SportmonksMsGetStintsByFixtureAndDriverParams, + SportmonksMsGetStintsByFixtureAndDriverResponse +> = { + id: 'sportmonks_motorsport_get_stints_by_fixture_and_driver', + name: 'Get Stints by Fixture and Driver', + description: 'Retrieve all tyre stints for a motorsport fixture and driver from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + driverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the driver', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/stints/drivers/${encodeURIComponent(params.driverId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stints_by_fixture_and_driver') + } + return { + success: true, + output: { + stints: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + stints: { + type: 'array', + description: 'Array of stint objects for the fixture and driver', + items: { type: 'object', properties: SPORTMONKS_MS_STINT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_stint.ts b/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_stint.ts new file mode 100644 index 00000000000..1f6798ed26d --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_stints_by_fixture_and_stint.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STINT_PROPERTIES, + type SportmonksMsStint, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetStintsByFixtureAndStintParams extends SportmonksBaseParams { + fixtureId: string + stintNumber: string +} + +export interface SportmonksMsGetStintsByFixtureAndStintResponse extends ToolResponse { + output: { + stints: SportmonksMsStint[] + } +} + +export const sportmonksMotorsportGetStintsByFixtureAndStintTool: ToolConfig< + SportmonksMsGetStintsByFixtureAndStintParams, + SportmonksMsGetStintsByFixtureAndStintResponse +> = { + id: 'sportmonks_motorsport_get_stints_by_fixture_and_stint', + name: 'Get Stints by Fixture and Stint Number', + description: 'Retrieve all tyre stints for a motorsport fixture and stint number from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture (session)', + }, + stintNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The stint number to retrieve', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. participant;details)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/fixtures/${encodeURIComponent(params.fixtureId.trim())}/stints/${encodeURIComponent(params.stintNumber.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_stints_by_fixture_and_stint') + } + return { + success: true, + output: { + stints: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + stints: { + type: 'array', + description: 'Array of stint objects for the fixture and stint number', + items: { type: 'object', properties: SPORTMONKS_MS_STINT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_team_standings.ts b/apps/sim/tools/sportmonks_motorsport/get_team_standings.ts new file mode 100644 index 00000000000..b0f74b466b7 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_team_standings.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STANDING_PROPERTIES, + type SportmonksMsStanding, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetTeamStandingsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetTeamStandingsResponse extends ToolResponse { + output: { + standings: SportmonksMsStanding[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetTeamStandingsTool: ToolConfig< + SportmonksMsGetTeamStandingsParams, + SportmonksMsGetTeamStandingsResponse +> = { + id: 'sportmonks_motorsport_get_team_standings', + name: 'Get All Team Standings', + description: 'Retrieve all team (constructor) championship standings from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. participant;season)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/standings/teams`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_team_standings') + } + return { + success: true, + output: { + standings: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + standings: { + type: 'array', + description: 'Array of team (constructor) standing entries', + items: { type: 'object', properties: SPORTMONKS_MS_STANDING_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_teams_by_country.ts b/apps/sim/tools/sportmonks_motorsport/get_teams_by_country.ts new file mode 100644 index 00000000000..38ea78210c0 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_teams_by_country.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_TEAM_PROPERTIES, + type SportmonksMsTeam, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetTeamsByCountryParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + countryId: string +} + +export interface SportmonksMsGetTeamsByCountryResponse extends ToolResponse { + output: { + teams: SportmonksMsTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetTeamsByCountryTool: ToolConfig< + SportmonksMsGetTeamsByCountryParams, + SportmonksMsGetTeamsByCountryResponse +> = { + id: 'sportmonks_motorsport_get_teams_by_country', + name: 'Get Teams by Country', + description: + 'Retrieve all motorsport teams (constructors) for a country by country ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + countryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the country', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;drivers)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/teams/countries/${encodeURIComponent(params.countryId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_teams_by_country') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team (constructor) objects for the country', + items: { type: 'object', properties: SPORTMONKS_MS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_teams_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_teams_by_season.ts new file mode 100644 index 00000000000..cf651b51c1b --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_teams_by_season.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_TEAM_PROPERTIES, + type SportmonksMsTeam, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetTeamsBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetTeamsBySeasonResponse extends ToolResponse { + output: { + teams: SportmonksMsTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetTeamsBySeasonTool: ToolConfig< + SportmonksMsGetTeamsBySeasonParams, + SportmonksMsGetTeamsBySeasonResponse +> = { + id: 'sportmonks_motorsport_get_teams_by_season', + name: 'Get Teams by Season', + description: + 'Retrieve all motorsport teams (constructors) for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;drivers)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/teams/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_teams_by_season') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team (constructor) objects for the season', + items: { type: 'object', properties: SPORTMONKS_MS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_venue.ts b/apps/sim/tools/sportmonks_motorsport/get_venue.ts new file mode 100644 index 00000000000..a603b448cbb --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_venue.ts @@ -0,0 +1,89 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_VENUE_PROPERTIES, + type SportmonksMsVenue, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetVenueParams extends SportmonksBaseParams { + venueId: string +} + +export interface SportmonksMsGetVenueResponse extends ToolResponse { + output: { + venue: SportmonksMsVenue | null + } +} + +export const sportmonksMotorsportGetVenueTool: ToolConfig< + SportmonksMsGetVenueParams, + SportmonksMsGetVenueResponse +> = { + id: 'sportmonks_motorsport_get_venue', + name: 'Get Venue by ID', + description: 'Retrieve a single motorsport venue (racing track) by its ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + venueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the venue (track)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/venues/${encodeURIComponent(params.venueId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_venue') + } + return { + success: true, + output: { + venue: data.data ?? null, + }, + } + }, + + outputs: { + venue: { + type: 'object', + description: 'The requested venue (racing track) object', + properties: SPORTMONKS_MS_VENUE_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_venues.ts b/apps/sim/tools/sportmonks_motorsport/get_venues.ts new file mode 100644 index 00000000000..c1cb8c93bc4 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_venues.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_VENUE_PROPERTIES, + type SportmonksMsVenue, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetVenuesParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksMsGetVenuesResponse extends ToolResponse { + output: { + venues: SportmonksMsVenue[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetVenuesTool: ToolConfig< + SportmonksMsGetVenuesParams, + SportmonksMsGetVenuesResponse +> = { + id: 'sportmonks_motorsport_get_venues', + name: 'Get Venues', + description: 'Retrieve all motorsport venues (racing tracks) from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_MOTORSPORT_BASE_URL}/venues`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_venues') + } + return { + success: true, + output: { + venues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + venues: { + type: 'array', + description: 'Array of venue (racing track) objects', + items: { type: 'object', properties: SPORTMONKS_MS_VENUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/get_venues_by_season.ts b/apps/sim/tools/sportmonks_motorsport/get_venues_by_season.ts new file mode 100644 index 00000000000..2a8d92fcc09 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/get_venues_by_season.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_VENUE_PROPERTIES, + type SportmonksMsVenue, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsGetVenuesBySeasonParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + seasonId: string +} + +export interface SportmonksMsGetVenuesBySeasonResponse extends ToolResponse { + output: { + venues: SportmonksMsVenue[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportGetVenuesBySeasonTool: ToolConfig< + SportmonksMsGetVenuesBySeasonParams, + SportmonksMsGetVenuesBySeasonResponse +> = { + id: 'sportmonks_motorsport_get_venues_by_season', + name: 'Get Venues by Season', + description: + 'Retrieve all motorsport venues (racing tracks) for a season by season ID from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + seasonId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the season', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/venues/seasons/${encodeURIComponent(params.seasonId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_venues_by_season') + } + return { + success: true, + output: { + venues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + venues: { + type: 'array', + description: 'Array of venue (racing track) objects for the season', + items: { type: 'object', properties: SPORTMONKS_MS_VENUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/index.ts b/apps/sim/tools/sportmonks_motorsport/index.ts index acc0a9846ba..c208d12a2fd 100644 --- a/apps/sim/tools/sportmonks_motorsport/index.ts +++ b/apps/sim/tools/sportmonks_motorsport/index.ts @@ -1,12 +1,57 @@ +export { sportmonksMotorsportGetAllFixturesTool } from './get_all_fixtures' +export { sportmonksMotorsportGetCurrentLeaguesByTeamTool } from './get_current_leagues_by_team' export { sportmonksMotorsportGetDriverTool } from './get_driver' +export { sportmonksMotorsportGetDriverStandingsTool } from './get_driver_standings' export { sportmonksMotorsportGetDriverStandingsBySeasonTool } from './get_driver_standings_by_season' export { sportmonksMotorsportGetDriversTool } from './get_drivers' +export { sportmonksMotorsportGetDriversByCountryTool } from './get_drivers_by_country' +export { sportmonksMotorsportGetDriversBySeasonTool } from './get_drivers_by_season' export { sportmonksMotorsportGetFixtureTool } from './get_fixture' export { sportmonksMotorsportGetFixturesByDateTool } from './get_fixtures_by_date' +export { sportmonksMotorsportGetFixturesByDateRangeTool } from './get_fixtures_by_date_range' +export { sportmonksMotorsportGetFixturesByIdsTool } from './get_fixtures_by_ids' export { sportmonksMotorsportGetLapsByFixtureTool } from './get_laps_by_fixture' +export { sportmonksMotorsportGetLapsByFixtureAndDriverTool } from './get_laps_by_fixture_and_driver' +export { sportmonksMotorsportGetLapsByFixtureAndLapTool } from './get_laps_by_fixture_and_lap' +export { sportmonksMotorsportGetLatestLapsByFixtureTool } from './get_latest_laps_by_fixture' +export { sportmonksMotorsportGetLatestPitstopsByFixtureTool } from './get_latest_pitstops_by_fixture' +export { sportmonksMotorsportGetLatestStintsByFixtureTool } from './get_latest_stints_by_fixture' +export { sportmonksMotorsportGetLatestUpdatedDriversTool } from './get_latest_updated_drivers' +export { sportmonksMotorsportGetLatestUpdatedFixturesTool } from './get_latest_updated_fixtures' +export { sportmonksMotorsportGetLeagueTool } from './get_league' +export { sportmonksMotorsportGetLeaguesTool } from './get_leagues' +export { sportmonksMotorsportGetLeaguesByCountryTool } from './get_leagues_by_country' +export { sportmonksMotorsportGetLeaguesByDateTool } from './get_leagues_by_date' +export { sportmonksMotorsportGetLeaguesByLiveTool } from './get_leagues_by_live' +export { sportmonksMotorsportGetLeaguesByTeamTool } from './get_leagues_by_team' export { sportmonksMotorsportGetLivescoresTool } from './get_livescores' export { sportmonksMotorsportGetPitstopsByFixtureTool } from './get_pitstops_by_fixture' +export { sportmonksMotorsportGetPitstopsByFixtureAndDriverTool } from './get_pitstops_by_fixture_and_driver' +export { sportmonksMotorsportGetPitstopsByFixtureAndLapTool } from './get_pitstops_by_fixture_and_lap' +export { sportmonksMotorsportGetRaceResultsBySeasonAndDriverTool } from './get_race_results_by_season_and_driver' +export { sportmonksMotorsportGetRaceResultsBySeasonAndTeamTool } from './get_race_results_by_season_and_team' +export { sportmonksMotorsportGetSchedulesBySeasonTool } from './get_schedules_by_season' +export { sportmonksMotorsportGetSeasonTool } from './get_season' +export { sportmonksMotorsportGetSeasonsTool } from './get_seasons' +export { sportmonksMotorsportGetStageTool } from './get_stage' +export { sportmonksMotorsportGetStagesTool } from './get_stages' +export { sportmonksMotorsportGetStagesBySeasonTool } from './get_stages_by_season' +export { sportmonksMotorsportGetStateTool } from './get_state' +export { sportmonksMotorsportGetStatesTool } from './get_states' +export { sportmonksMotorsportGetStintsByFixtureTool } from './get_stints_by_fixture' +export { sportmonksMotorsportGetStintsByFixtureAndDriverTool } from './get_stints_by_fixture_and_driver' +export { sportmonksMotorsportGetStintsByFixtureAndStintTool } from './get_stints_by_fixture_and_stint' export { sportmonksMotorsportGetTeamTool } from './get_team' +export { sportmonksMotorsportGetTeamStandingsTool } from './get_team_standings' export { sportmonksMotorsportGetTeamStandingsBySeasonTool } from './get_team_standings_by_season' export { sportmonksMotorsportGetTeamsTool } from './get_teams' +export { sportmonksMotorsportGetTeamsByCountryTool } from './get_teams_by_country' +export { sportmonksMotorsportGetTeamsBySeasonTool } from './get_teams_by_season' +export { sportmonksMotorsportGetVenueTool } from './get_venue' +export { sportmonksMotorsportGetVenuesTool } from './get_venues' +export { sportmonksMotorsportGetVenuesBySeasonTool } from './get_venues_by_season' export { sportmonksMotorsportSearchDriversTool } from './search_drivers' +export { sportmonksMotorsportSearchLeaguesTool } from './search_leagues' +export { sportmonksMotorsportSearchStagesTool } from './search_stages' +export { sportmonksMotorsportSearchTeamsTool } from './search_teams' +export { sportmonksMotorsportSearchVenuesTool } from './search_venues' diff --git a/apps/sim/tools/sportmonks_motorsport/search_drivers.ts b/apps/sim/tools/sportmonks_motorsport/search_drivers.ts index 2680b01e0ff..d8e9a942f16 100644 --- a/apps/sim/tools/sportmonks_motorsport/search_drivers.ts +++ b/apps/sim/tools/sportmonks_motorsport/search_drivers.ts @@ -55,6 +55,12 @@ export const sportmonksMotorsportSearchDriversTool: ToolConfig< visibility: 'user-or-llm', description: 'Semicolon-separated relations to enrich the response (e.g. country;teams)', }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, per_page: { type: 'string', required: false, diff --git a/apps/sim/tools/sportmonks_motorsport/search_leagues.ts b/apps/sim/tools/sportmonks_motorsport/search_leagues.ts new file mode 100644 index 00000000000..83a85dd6957 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/search_leagues.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_LEAGUE_PROPERTIES, + type SportmonksMsLeague, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsSearchLeaguesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksMsSearchLeaguesResponse extends ToolResponse { + output: { + leagues: SportmonksMsLeague[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportSearchLeaguesTool: ToolConfig< + SportmonksMsSearchLeaguesParams, + SportmonksMsSearchLeaguesResponse +> = { + id: 'sportmonks_motorsport_search_leagues', + name: 'Search Leagues', + description: 'Search for motorsport leagues by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The league name to search for (e.g. Formula)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;seasons)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/leagues/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_leagues') + } + return { + success: true, + output: { + leagues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + leagues: { + type: 'array', + description: 'Array of league objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_MS_LEAGUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/search_stages.ts b/apps/sim/tools/sportmonks_motorsport/search_stages.ts new file mode 100644 index 00000000000..5aa917bdf86 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/search_stages.ts @@ -0,0 +1,116 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_STAGE_PROPERTIES, + type SportmonksMsStage, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsSearchStagesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksMsSearchStagesResponse extends ToolResponse { + output: { + stages: SportmonksMsStage[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportSearchStagesTool: ToolConfig< + SportmonksMsSearchStagesParams, + SportmonksMsSearchStagesResponse +> = { + id: 'sportmonks_motorsport_search_stages', + name: 'Search Stages', + description: 'Search for motorsport stages (race weekends) by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The stage name to search for (e.g. Monaco)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Semicolon-separated relations to enrich the response (e.g. league;season;fixtures)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/stages/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_stages') + } + return { + success: true, + output: { + stages: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + stages: { + type: 'array', + description: 'Array of stage (race weekend) objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_MS_STAGE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/search_teams.ts b/apps/sim/tools/sportmonks_motorsport/search_teams.ts new file mode 100644 index 00000000000..748903a1994 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/search_teams.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_TEAM_PROPERTIES, + type SportmonksMsTeam, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsSearchTeamsParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksMsSearchTeamsResponse extends ToolResponse { + output: { + teams: SportmonksMsTeam[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportSearchTeamsTool: ToolConfig< + SportmonksMsSearchTeamsParams, + SportmonksMsSearchTeamsResponse +> = { + id: 'sportmonks_motorsport_search_teams', + name: 'Search Teams', + description: 'Search for motorsport teams (constructors) by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The team name to search for (e.g. Bull)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;drivers)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/teams/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_teams') + } + return { + success: true, + output: { + teams: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of team (constructor) objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_MS_TEAM_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/search_venues.ts b/apps/sim/tools/sportmonks_motorsport/search_venues.ts new file mode 100644 index 00000000000..16e2a038be6 --- /dev/null +++ b/apps/sim/tools/sportmonks_motorsport/search_venues.ts @@ -0,0 +1,115 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_MOTORSPORT_BASE_URL, + SPORTMONKS_MS_VENUE_PROPERTIES, + type SportmonksMsVenue, +} from '@/tools/sportmonks_motorsport/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksMsSearchVenuesParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + query: string +} + +export interface SportmonksMsSearchVenuesResponse extends ToolResponse { + output: { + venues: SportmonksMsVenue[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksMotorsportSearchVenuesTool: ToolConfig< + SportmonksMsSearchVenuesParams, + SportmonksMsSearchVenuesResponse +> = { + id: 'sportmonks_motorsport_search_venues', + name: 'Search Venues', + description: 'Search for motorsport venues (racing tracks) by name from Sportmonks', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The venue name to search for (e.g. Hungaroring)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. country;city)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_MOTORSPORT_BASE_URL}/venues/search/${encodeURIComponent(params.query.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'search_venues') + } + return { + success: true, + output: { + venues: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + venues: { + type: 'array', + description: 'Array of venue (racing track) objects matching the search query', + items: { type: 'object', properties: SPORTMONKS_MS_VENUE_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_motorsport/types.ts b/apps/sim/tools/sportmonks_motorsport/types.ts index 51947e9e6ba..45a0d1b10e9 100644 --- a/apps/sim/tools/sportmonks_motorsport/types.ts +++ b/apps/sim/tools/sportmonks_motorsport/types.ts @@ -315,3 +315,259 @@ export interface SportmonksMsLap { participant_id: number is_latest: boolean } + +/** + * Output property definitions for a Stint object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/stint + */ +export const SPORTMONKS_MS_STINT_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the stint' }, + fixture_id: { type: 'number', description: 'Fixture related to the stint' }, + stint_number: { type: 'number', description: 'Stint number in the fixture' }, + driver_number: { type: 'number', description: 'Number of the driver' }, + participant_id: { type: 'number', description: 'Driver related to the stint' }, + is_latest: { type: 'boolean', description: 'Whether it is the latest stint' }, +} as const satisfies Record + +/** + * Output property definitions for a Venue (racing track) object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/venue + */ +export const SPORTMONKS_MS_VENUE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the venue (track)' }, + country_id: { type: 'number', description: 'Country the venue is in' }, + city_id: { + type: 'number', + description: 'City the venue is in', + nullable: true, + optional: true, + }, + name: { type: 'string', description: 'Name of the venue/track' }, + address: { type: 'string', description: 'Address of the venue', nullable: true }, + zipcode: { type: 'string', description: 'Zipcode of the venue', nullable: true }, + latitude: { type: 'string', description: 'Latitude of the venue', nullable: true }, + longitude: { type: 'string', description: 'Longitude of the venue', nullable: true }, + capacity: { type: 'number', description: 'Capacity of the venue', nullable: true }, + image_path: { + type: 'string', + description: 'URL to the track layout image', + nullable: true, + optional: true, + }, + city_name: { type: 'string', description: 'Name of the city the venue is in', nullable: true }, + surface: { type: 'string', description: 'Surface of the venue', nullable: true }, + national_team: { + type: 'boolean', + description: 'Not used in the Motorsport API', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Motorsport League object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/league + */ +export const SPORTMONKS_MS_LEAGUE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the league' }, + sport_id: { type: 'number', description: 'Sport of the league' }, + country_id: { type: 'number', description: 'Country of the league' }, + name: { type: 'string', description: 'Name of the league' }, + active: { type: 'boolean', description: 'Whether the league is active' }, + short_code: { type: 'string', description: 'Short code of the league', nullable: true }, + image_path: { + type: 'string', + description: 'URL to the league logo', + nullable: true, + optional: true, + }, + type: { type: 'string', description: 'Type of the league', optional: true }, + sub_type: { + type: 'string', + description: 'Subtype of the league', + nullable: true, + optional: true, + }, + last_played_at: { + type: 'string', + description: 'Date of the last fixture held in the league', + nullable: true, + }, + category: { + type: 'number', + description: 'Category of the league', + nullable: true, + optional: true, + }, + has_jerseys: { + type: 'boolean', + description: 'Not used in the Motorsport API', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Motorsport Season object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/season + */ +export const SPORTMONKS_MS_SEASON_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the season' }, + sport_id: { type: 'number', description: 'Sport of the season' }, + league_id: { type: 'number', description: 'League of the season' }, + tie_breaker_rule_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, + name: { type: 'string', description: 'Name of the season' }, + finished: { type: 'boolean', description: 'Whether the season is finished' }, + pending: { type: 'boolean', description: 'Whether the season is pending' }, + is_current: { type: 'boolean', description: 'Whether the season is the current season' }, + starting_at: { type: 'string', description: 'Starting date of the season', nullable: true }, + ending_at: { type: 'string', description: 'Ending date of the season', nullable: true }, + standings_recalculated_at: { + type: 'string', + description: 'Timestamp when standings were last updated', + nullable: true, + optional: true, + }, + games_in_current_week: { + type: 'boolean', + description: 'Not used in the Motorsport API', + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Motorsport Stage (race weekend) object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/stage + */ +export const SPORTMONKS_MS_STAGE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the stage (race weekend)' }, + sport_id: { type: 'number', description: 'Sport of the stage' }, + league_id: { type: 'number', description: 'League related to the stage' }, + season_id: { type: 'number', description: 'Season related to the stage' }, + type_id: { type: 'number', description: 'Type of the stage', nullable: true }, + name: { type: 'string', description: 'Name of the stage' }, + sort_order: { + type: 'number', + description: 'Order of the stage', + nullable: true, + optional: true, + }, + finished: { type: 'boolean', description: 'Whether the stage is finished' }, + is_current: { type: 'boolean', description: 'Whether the stage is the current stage' }, + starting_at: { type: 'string', description: 'Starting date of the stage', nullable: true }, + ending_at: { type: 'string', description: 'Ending date of the stage', nullable: true }, + games_in_current_week: { + type: 'boolean', + description: 'Not used in the Motorsport API', + optional: true, + }, + tie_breaker_rule_id: { + type: 'number', + description: 'Not used in the Motorsport API', + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Motorsport State (fixture status) object. + * @see https://docs.sportmonks.com/v3/motorsport-api/endpoints-and-entities/entities/state + */ +export const SPORTMONKS_MS_STATE_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the state' }, + state: { type: 'string', description: 'Abbreviation of the state' }, + name: { type: 'string', description: 'Full name of the state' }, + short_name: { + type: 'string', + description: 'Short name of the state', + nullable: true, + optional: true, + }, + developer_name: { + type: 'string', + description: 'Name recommended for developers to use', + optional: true, + }, +} as const satisfies Record + +export interface SportmonksMsStint { + id: number + fixture_id: number + stint_number: number + driver_number: number + participant_id: number + is_latest: boolean +} + +export interface SportmonksMsVenue { + id: number + country_id: number + city_id?: number | null + name: string + address: string | null + zipcode: string | null + latitude: string | null + longitude: string | null + capacity: number | null + image_path?: string | null + city_name: string | null + surface: string | null + national_team?: boolean +} + +export interface SportmonksMsLeague { + id: number + sport_id: number + country_id: number + name: string + active: boolean + short_code: string | null + image_path?: string | null + type?: string + sub_type?: string | null + last_played_at: string | null + category?: number | null + has_jerseys?: boolean +} + +export interface SportmonksMsSeason { + id: number + sport_id: number + league_id: number + tie_breaker_rule_id?: number | null + name: string + finished: boolean + pending: boolean + is_current: boolean + starting_at: string | null + ending_at: string | null + standings_recalculated_at?: string | null + games_in_current_week?: boolean +} + +export interface SportmonksMsStage { + id: number + sport_id: number + league_id: number + season_id: number + type_id: number | null + name: string + sort_order?: number | null + finished: boolean + is_current: boolean + starting_at: string | null + ending_at: string | null + games_in_current_week?: boolean + tie_breaker_rule_id?: number | null +} + +export interface SportmonksMsState { + id: number + state: string + name: string + short_name?: string | null + developer_name?: string +} diff --git a/apps/sim/tools/sportmonks_odds/get_all_historical_odds.ts b/apps/sim/tools/sportmonks_odds/get_all_historical_odds.ts new file mode 100644 index 00000000000..35642860961 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_all_historical_odds.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_HISTORY_PROPERTIES, + type SportmonksPremiumOddHistory, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllHistoricalOddsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllHistoricalOddsResponse extends ToolResponse { + output: { + historicalOdds: SportmonksPremiumOddHistory[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetAllHistoricalOddsTool: ToolConfig< + SportmonksGetAllHistoricalOddsParams, + SportmonksGetAllHistoricalOddsResponse +> = { + id: 'sportmonks_odds_get_all_historical_odds', + name: 'Get All Historical Odds', + description: + 'Retrieve all available historical (premium) pre-match odd values from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. odd)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. winningOdds)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium/history`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_historical_odds') + } + return { + success: true, + output: { + historicalOdds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + historicalOdds: { + type: 'array', + description: 'Array of historical premium odd value records', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_HISTORY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_all_inplay_odds.ts b/apps/sim/tools/sportmonks_odds/get_all_inplay_odds.ts new file mode 100644 index 00000000000..3ec84e013d0 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_all_inplay_odds.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_INPLAY_ODD_PROPERTIES, + type SportmonksInplayOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllInplayOddsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllInplayOddsResponse extends ToolResponse { + output: { + odds: SportmonksInplayOdd[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetAllInplayOddsTool: ToolConfig< + SportmonksGetAllInplayOddsParams, + SportmonksGetAllInplayOddsResponse +> = { + id: 'sportmonks_odds_get_all_inplay_odds', + name: 'Get All In-play Odds', + description: 'Retrieve all available live (in-play) odds from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12, bookmakers:2,14, IdAfter:oddID)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/inplay`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_inplay_odds') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of in-play odd objects', + items: { type: 'object', properties: SPORTMONKS_INPLAY_ODD_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_all_pre_match_odds.ts b/apps/sim/tools/sportmonks_odds/get_all_pre_match_odds.ts new file mode 100644 index 00000000000..cb60cfa77da --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_all_pre_match_odds.ts @@ -0,0 +1,106 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_ODD_PROPERTIES, + type SportmonksOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllPreMatchOddsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllPreMatchOddsResponse extends ToolResponse { + output: { + odds: SportmonksOdd[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetAllPreMatchOddsTool: ToolConfig< + SportmonksGetAllPreMatchOddsParams, + SportmonksGetAllPreMatchOddsResponse +> = { + id: 'sportmonks_odds_get_all_pre_match_odds', + name: 'Get All Pre-match Odds', + description: 'Retrieve all available pre-match odds from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filters to apply (e.g. markets:1,12, bookmakers:2,14, winningOdds, IdAfter:oddID)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/pre-match`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_pre_match_odds') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of pre-match odd objects', + items: { type: 'object', properties: SPORTMONKS_ODD_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_all_premium_odds.ts b/apps/sim/tools/sportmonks_odds/get_all_premium_odds.ts new file mode 100644 index 00000000000..cb9250fd4a1 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_all_premium_odds.ts @@ -0,0 +1,105 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_PROPERTIES, + type SportmonksPremiumOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetAllPremiumOddsParams + extends SportmonksBaseParams, + SportmonksPaginationParams {} + +export interface SportmonksGetAllPremiumOddsResponse extends ToolResponse { + output: { + premiumOdds: SportmonksPremiumOdd[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetAllPremiumOddsTool: ToolConfig< + SportmonksGetAllPremiumOddsParams, + SportmonksGetAllPremiumOddsResponse +> = { + id: 'sportmonks_odds_get_all_premium_odds', + name: 'Get All Premium Odds', + description: + 'Retrieve all available premium (historical) pre-match odds from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12, bookmakers:2,14, IdAfter:oddID)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_all_premium_odds') + } + return { + success: true, + output: { + premiumOdds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + premiumOdds: { + type: 'array', + description: 'Array of premium odd objects', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_bookmaker_event_ids_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_bookmaker_event_ids_by_fixture.ts new file mode 100644 index 00000000000..b6f59054b29 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_bookmaker_event_ids_by_fixture.ts @@ -0,0 +1,104 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_BOOKMAKER_EVENT_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksBookmakerEvent, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetBookmakerEventIdsByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetBookmakerEventIdsByFixtureResponse extends ToolResponse { + output: { + bookmakerEvents: SportmonksBookmakerEvent[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetBookmakerEventIdsByFixtureTool: ToolConfig< + SportmonksGetBookmakerEventIdsByFixtureParams, + SportmonksGetBookmakerEventIdsByFixtureResponse +> = { + id: 'sportmonks_odds_get_bookmaker_event_ids_by_fixture', + name: 'Get Bookmaker Event IDs by Fixture', + description: + "Retrieve bookmakers' own event ids mapped to a Sportmonks fixture via the Sportmonks Odds API", + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_ODDS_BASE_URL}/bookmakers/fixtures/${encodeURIComponent(params.fixtureId.trim())}/mapping` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_bookmaker_event_ids_by_fixture') + } + return { + success: true, + output: { + bookmakerEvents: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + bookmakerEvents: { + type: 'array', + description: 'Array of bookmaker event mapping records for the fixture', + items: { type: 'object', properties: SPORTMONKS_BOOKMAKER_EVENT_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_bookmakers_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_bookmakers_by_fixture.ts new file mode 100644 index 00000000000..8fa2123adce --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_bookmakers_by_fixture.ts @@ -0,0 +1,103 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_BOOKMAKER_PROPERTIES, + SPORTMONKS_ODDS_BASE_URL, + type SportmonksBookmaker, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetBookmakersByFixtureParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fixtureId: string +} + +export interface SportmonksGetBookmakersByFixtureResponse extends ToolResponse { + output: { + bookmakers: SportmonksBookmaker[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetBookmakersByFixtureTool: ToolConfig< + SportmonksGetBookmakersByFixtureParams, + SportmonksGetBookmakersByFixtureResponse +> = { + id: 'sportmonks_odds_get_bookmakers_by_fixture', + name: 'Get Bookmakers by Fixture', + description: 'Retrieve all bookmakers available for a fixture from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_ODDS_BASE_URL}/bookmakers/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_bookmakers_by_fixture') + } + return { + success: true, + output: { + bookmakers: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + bookmakers: { + type: 'array', + description: 'Array of bookmaker objects available for the fixture', + items: { type: 'object', properties: SPORTMONKS_BOOKMAKER_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts index 72f610a6b93..5ee24da541e 100644 --- a/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts +++ b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture.ts @@ -8,8 +8,8 @@ import { type SportmonksPaginationParams, } from '@/tools/sportmonks/types' import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, SPORTMONKS_INPLAY_ODD_PROPERTIES, - SPORTMONKS_ODDS_BASE_URL, type SportmonksInplayOdd, } from '@/tools/sportmonks_odds/types' import type { ToolConfig, ToolResponse } from '@/tools/types' @@ -84,7 +84,7 @@ export const sportmonksOddsGetInplayOddsByFixtureTool: ToolConfig< request: { url: (params) => { - const url = `${SPORTMONKS_ODDS_BASE_URL}/inplay/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/inplay/fixtures/${encodeURIComponent(params.fixtureId.trim())}` return appendSportmonksQuery(url, params) }, method: 'GET', diff --git a/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_bookmaker.ts b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_bookmaker.ts new file mode 100644 index 00000000000..fb38e24ab35 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_bookmaker.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_INPLAY_ODD_PROPERTIES, + type SportmonksInplayOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetInplayOddsByFixtureAndBookmakerParams extends SportmonksBaseParams { + fixtureId: string + bookmakerId: string +} + +export interface SportmonksGetInplayOddsByFixtureAndBookmakerResponse extends ToolResponse { + output: { + odds: SportmonksInplayOdd[] + } +} + +export const sportmonksOddsGetInplayOddsByFixtureAndBookmakerTool: ToolConfig< + SportmonksGetInplayOddsByFixtureAndBookmakerParams, + SportmonksGetInplayOddsByFixtureAndBookmakerResponse +> = { + id: 'sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker', + name: 'Get In-play Odds by Fixture and Bookmaker', + description: + 'Retrieve live (in-play) odds for a fixture from a specific bookmaker via the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + bookmakerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the bookmaker', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/inplay/fixtures/${encodeURIComponent(params.fixtureId.trim())}/bookmakers/${encodeURIComponent(params.bookmakerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_inplay_odds_by_fixture_and_bookmaker') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of in-play odd objects for the fixture and bookmaker', + items: { type: 'object', properties: SPORTMONKS_INPLAY_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_market.ts b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_market.ts new file mode 100644 index 00000000000..ca207ee94ac --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_inplay_odds_by_fixture_and_market.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_INPLAY_ODD_PROPERTIES, + type SportmonksInplayOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetInplayOddsByFixtureAndMarketParams extends SportmonksBaseParams { + fixtureId: string + marketId: string +} + +export interface SportmonksGetInplayOddsByFixtureAndMarketResponse extends ToolResponse { + output: { + odds: SportmonksInplayOdd[] + } +} + +export const sportmonksOddsGetInplayOddsByFixtureAndMarketTool: ToolConfig< + SportmonksGetInplayOddsByFixtureAndMarketParams, + SportmonksGetInplayOddsByFixtureAndMarketResponse +> = { + id: 'sportmonks_odds_get_inplay_odds_by_fixture_and_market', + name: 'Get In-play Odds by Fixture and Market', + description: + 'Retrieve live (in-play) odds for a fixture on a specific market via the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + marketId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the market', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. bookmakers:2,14)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/inplay/fixtures/${encodeURIComponent(params.fixtureId.trim())}/markets/${encodeURIComponent(params.marketId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_inplay_odds_by_fixture_and_market') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of in-play odd objects for the fixture and market', + items: { type: 'object', properties: SPORTMONKS_INPLAY_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_last_updated_inplay_odds.ts b/apps/sim/tools/sportmonks_odds/get_last_updated_inplay_odds.ts new file mode 100644 index 00000000000..f6c5e196ff3 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_last_updated_inplay_odds.ts @@ -0,0 +1,79 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_INPLAY_ODD_PROPERTIES, + type SportmonksInplayOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLastUpdatedInplayOddsParams extends SportmonksBaseParams {} + +export interface SportmonksGetLastUpdatedInplayOddsResponse extends ToolResponse { + output: { + odds: SportmonksInplayOdd[] + } +} + +export const sportmonksOddsGetLastUpdatedInplayOddsTool: ToolConfig< + SportmonksGetLastUpdatedInplayOddsParams, + SportmonksGetLastUpdatedInplayOddsResponse +> = { + id: 'sportmonks_odds_get_last_updated_inplay_odds', + name: 'Get Last Updated In-play Odds', + description: 'Retrieve in-play odds updated in the last 10 seconds from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or bookmakers:2,14)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/inplay/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_last_updated_inplay_odds') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of in-play odd objects updated in the last 10 seconds', + items: { type: 'object', properties: SPORTMONKS_INPLAY_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_last_updated_pre_match_odds.ts b/apps/sim/tools/sportmonks_odds/get_last_updated_pre_match_odds.ts new file mode 100644 index 00000000000..9f48ef582fe --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_last_updated_pre_match_odds.ts @@ -0,0 +1,80 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_ODD_PROPERTIES, + type SportmonksOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetLastUpdatedPreMatchOddsParams extends SportmonksBaseParams {} + +export interface SportmonksGetLastUpdatedPreMatchOddsResponse extends ToolResponse { + output: { + odds: SportmonksOdd[] + } +} + +export const sportmonksOddsGetLastUpdatedPreMatchOddsTool: ToolConfig< + SportmonksGetLastUpdatedPreMatchOddsParams, + SportmonksGetLastUpdatedPreMatchOddsResponse +> = { + id: 'sportmonks_odds_get_last_updated_pre_match_odds', + name: 'Get Last Updated Pre-match Odds', + description: + 'Retrieve pre-match odds updated in the last 10 seconds from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or bookmakers:2,14 or winningOdds)', + }, + }, + + request: { + url: (params) => + appendSportmonksQuery(`${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/pre-match/latest`, params), + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_last_updated_pre_match_odds') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of pre-match odd objects updated in the last 10 seconds', + items: { type: 'object', properties: SPORTMONKS_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts index f769d06bf63..9b9df64361f 100644 --- a/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts +++ b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture.ts @@ -8,8 +8,8 @@ import { type SportmonksPaginationParams, } from '@/tools/sportmonks/types' import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, SPORTMONKS_ODD_PROPERTIES, - SPORTMONKS_ODDS_BASE_URL, type SportmonksOdd, } from '@/tools/sportmonks_odds/types' import type { ToolConfig, ToolResponse } from '@/tools/types' @@ -83,7 +83,7 @@ export const sportmonksOddsGetPreMatchOddsByFixtureTool: ToolConfig< request: { url: (params) => { - const url = `${SPORTMONKS_ODDS_BASE_URL}/pre-match/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/pre-match/fixtures/${encodeURIComponent(params.fixtureId.trim())}` return appendSportmonksQuery(url, params) }, method: 'GET', diff --git a/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_bookmaker.ts b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_bookmaker.ts new file mode 100644 index 00000000000..9c0838194f5 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_bookmaker.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_ODD_PROPERTIES, + type SportmonksOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPreMatchOddsByFixtureAndBookmakerParams extends SportmonksBaseParams { + fixtureId: string + bookmakerId: string +} + +export interface SportmonksGetPreMatchOddsByFixtureAndBookmakerResponse extends ToolResponse { + output: { + odds: SportmonksOdd[] + } +} + +export const sportmonksOddsGetPreMatchOddsByFixtureAndBookmakerTool: ToolConfig< + SportmonksGetPreMatchOddsByFixtureAndBookmakerParams, + SportmonksGetPreMatchOddsByFixtureAndBookmakerResponse +> = { + id: 'sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker', + name: 'Get Pre-match Odds by Fixture and Bookmaker', + description: + 'Retrieve pre-match odds for a fixture from a specific bookmaker via the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + bookmakerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the bookmaker', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or winningOdds)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/pre-match/fixtures/${encodeURIComponent(params.fixtureId.trim())}/bookmakers/${encodeURIComponent(params.bookmakerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_pre_match_odds_by_fixture_and_bookmaker') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of pre-match odd objects for the fixture and bookmaker', + items: { type: 'object', properties: SPORTMONKS_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_market.ts b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_market.ts new file mode 100644 index 00000000000..5e57b824ae4 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_pre_match_odds_by_fixture_and_market.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_ODD_PROPERTIES, + type SportmonksOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPreMatchOddsByFixtureAndMarketParams extends SportmonksBaseParams { + fixtureId: string + marketId: string +} + +export interface SportmonksGetPreMatchOddsByFixtureAndMarketResponse extends ToolResponse { + output: { + odds: SportmonksOdd[] + } +} + +export const sportmonksOddsGetPreMatchOddsByFixtureAndMarketTool: ToolConfig< + SportmonksGetPreMatchOddsByFixtureAndMarketParams, + SportmonksGetPreMatchOddsByFixtureAndMarketResponse +> = { + id: 'sportmonks_odds_get_pre_match_odds_by_fixture_and_market', + name: 'Get Pre-match Odds by Fixture and Market', + description: + 'Retrieve pre-match odds for a fixture on a specific market via the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + marketId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the market', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. bookmakers:2,14 or winningOdds)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/pre-match/fixtures/${encodeURIComponent(params.fixtureId.trim())}/markets/${encodeURIComponent(params.marketId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_pre_match_odds_by_fixture_and_market') + } + return { + success: true, + output: { + odds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + odds: { + type: 'array', + description: 'Array of pre-match odd objects for the fixture and market', + items: { type: 'object', properties: SPORTMONKS_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture.ts b/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture.ts new file mode 100644 index 00000000000..faa9ca446a0 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture.ts @@ -0,0 +1,90 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_PROPERTIES, + type SportmonksPremiumOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPremiumOddsByFixtureParams extends SportmonksBaseParams { + fixtureId: string +} + +export interface SportmonksGetPremiumOddsByFixtureResponse extends ToolResponse { + output: { + premiumOdds: SportmonksPremiumOdd[] + } +} + +export const sportmonksOddsGetPremiumOddsByFixtureTool: ToolConfig< + SportmonksGetPremiumOddsByFixtureParams, + SportmonksGetPremiumOddsByFixtureResponse +> = { + id: 'sportmonks_odds_get_premium_odds_by_fixture', + name: 'Get Premium Odds by Fixture', + description: + 'Retrieve premium (historical) pre-match odds for a fixture from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or bookmakers:2,14)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium/fixtures/${encodeURIComponent(params.fixtureId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_premium_odds_by_fixture') + } + return { + success: true, + output: { + premiumOdds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + premiumOdds: { + type: 'array', + description: 'Array of premium odd objects for the fixture', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_bookmaker.ts b/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_bookmaker.ts new file mode 100644 index 00000000000..e288eeb08b9 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_bookmaker.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_PROPERTIES, + type SportmonksPremiumOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPremiumOddsByFixtureAndBookmakerParams extends SportmonksBaseParams { + fixtureId: string + bookmakerId: string +} + +export interface SportmonksGetPremiumOddsByFixtureAndBookmakerResponse extends ToolResponse { + output: { + premiumOdds: SportmonksPremiumOdd[] + } +} + +export const sportmonksOddsGetPremiumOddsByFixtureAndBookmakerTool: ToolConfig< + SportmonksGetPremiumOddsByFixtureAndBookmakerParams, + SportmonksGetPremiumOddsByFixtureAndBookmakerResponse +> = { + id: 'sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker', + name: 'Get Premium Odds by Fixture and Bookmaker', + description: + 'Retrieve premium pre-match odds for a fixture from a specific bookmaker via the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + bookmakerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the bookmaker', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium/fixtures/${encodeURIComponent(params.fixtureId.trim())}/bookmakers/${encodeURIComponent(params.bookmakerId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_premium_odds_by_fixture_and_bookmaker') + } + return { + success: true, + output: { + premiumOdds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + premiumOdds: { + type: 'array', + description: 'Array of premium odd objects for the fixture and bookmaker', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_market.ts b/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_market.ts new file mode 100644 index 00000000000..46ffa8c7851 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_premium_odds_by_fixture_and_market.ts @@ -0,0 +1,97 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + type SportmonksBaseParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_PROPERTIES, + type SportmonksPremiumOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetPremiumOddsByFixtureAndMarketParams extends SportmonksBaseParams { + fixtureId: string + marketId: string +} + +export interface SportmonksGetPremiumOddsByFixtureAndMarketResponse extends ToolResponse { + output: { + premiumOdds: SportmonksPremiumOdd[] + } +} + +export const sportmonksOddsGetPremiumOddsByFixtureAndMarketTool: ToolConfig< + SportmonksGetPremiumOddsByFixtureAndMarketParams, + SportmonksGetPremiumOddsByFixtureAndMarketResponse +> = { + id: 'sportmonks_odds_get_premium_odds_by_fixture_and_market', + name: 'Get Premium Odds by Fixture and Market', + description: + 'Retrieve premium pre-match odds for a fixture on a specific market via the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fixtureId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the fixture', + }, + marketId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique id of the market', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. bookmakers:2,14)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium/fixtures/${encodeURIComponent(params.fixtureId.trim())}/markets/${encodeURIComponent(params.marketId.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_premium_odds_by_fixture_and_market') + } + return { + success: true, + output: { + premiumOdds: Array.isArray(data.data) ? data.data : [], + }, + } + }, + + outputs: { + premiumOdds: { + type: 'array', + description: 'Array of premium odd objects for the fixture and market', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_updated_historical_odds_between.ts b/apps/sim/tools/sportmonks_odds/get_updated_historical_odds_between.ts new file mode 100644 index 00000000000..77cd07bf0fd --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_updated_historical_odds_between.ts @@ -0,0 +1,123 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_HISTORY_PROPERTIES, + type SportmonksPremiumOddHistory, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetUpdatedHistoricalOddsBetweenParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fromTimestamp: string + toTimestamp: string +} + +export interface SportmonksGetUpdatedHistoricalOddsBetweenResponse extends ToolResponse { + output: { + historicalOdds: SportmonksPremiumOddHistory[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetUpdatedHistoricalOddsBetweenTool: ToolConfig< + SportmonksGetUpdatedHistoricalOddsBetweenParams, + SportmonksGetUpdatedHistoricalOddsBetweenResponse +> = { + id: 'sportmonks_odds_get_updated_historical_odds_between', + name: 'Get Updated Historical Odds Between Time Range', + description: + 'Retrieve historical (premium) odds updated between two UNIX timestamps (max 5 minutes) from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fromTimestamp: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start of the range as a UNIX timestamp (e.g. 1767225600)', + }, + toTimestamp: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End of the range as a UNIX timestamp (max 5 minutes after the start)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. odd)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. winningOdds)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium/history/updated/between/${encodeURIComponent(params.fromTimestamp.trim())}/${encodeURIComponent(params.toTimestamp.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_updated_historical_odds_between') + } + return { + success: true, + output: { + historicalOdds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + historicalOdds: { + type: 'array', + description: 'Array of historical premium odd value records updated within the time range', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_HISTORY_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/get_updated_premium_odds_between.ts b/apps/sim/tools/sportmonks_odds/get_updated_premium_odds_between.ts new file mode 100644 index 00000000000..f12156812a3 --- /dev/null +++ b/apps/sim/tools/sportmonks_odds/get_updated_premium_odds_between.ts @@ -0,0 +1,123 @@ +import { + appendSportmonksQuery, + buildSportmonksHeaders, + handleSportmonksError, + SPORTMONKS_PAGINATION_OUTPUT, + type SportmonksBaseParams, + type SportmonksPagination, + type SportmonksPaginationParams, +} from '@/tools/sportmonks/types' +import { + SPORTMONKS_FOOTBALL_ODDS_BASE_URL, + SPORTMONKS_PREMIUM_ODD_PROPERTIES, + type SportmonksPremiumOdd, +} from '@/tools/sportmonks_odds/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SportmonksGetUpdatedPremiumOddsBetweenParams + extends SportmonksBaseParams, + SportmonksPaginationParams { + fromTimestamp: string + toTimestamp: string +} + +export interface SportmonksGetUpdatedPremiumOddsBetweenResponse extends ToolResponse { + output: { + premiumOdds: SportmonksPremiumOdd[] + pagination?: SportmonksPagination | null + } +} + +export const sportmonksOddsGetUpdatedPremiumOddsBetweenTool: ToolConfig< + SportmonksGetUpdatedPremiumOddsBetweenParams, + SportmonksGetUpdatedPremiumOddsBetweenResponse +> = { + id: 'sportmonks_odds_get_updated_premium_odds_between', + name: 'Get Updated Premium Odds Between Time Range', + description: + 'Retrieve premium odds updated between two UNIX timestamps (max 5 minutes) from the Sportmonks Odds API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sportmonks API token', + }, + fromTimestamp: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start of the range as a UNIX timestamp (e.g. 1767225600)', + }, + toTimestamp: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End of the range as a UNIX timestamp (max 5 minutes after the start)', + }, + include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated relations to enrich the response (e.g. market;bookmaker)', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filters to apply (e.g. markets:1,12 or bookmakers:2,14)', + }, + per_page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 50, default 25)', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number to retrieve', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, + }, + + request: { + url: (params) => { + const url = `${SPORTMONKS_FOOTBALL_ODDS_BASE_URL}/premium/updated/between/${encodeURIComponent(params.fromTimestamp.trim())}/${encodeURIComponent(params.toTimestamp.trim())}` + return appendSportmonksQuery(url, params) + }, + method: 'GET', + headers: (params) => buildSportmonksHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + handleSportmonksError(data, response.status, 'get_updated_premium_odds_between') + } + return { + success: true, + output: { + premiumOdds: Array.isArray(data.data) ? data.data : [], + pagination: data.pagination ?? null, + }, + } + }, + + outputs: { + premiumOdds: { + type: 'array', + description: 'Array of premium odd objects updated within the time range', + items: { type: 'object', properties: SPORTMONKS_PREMIUM_ODD_PROPERTIES }, + }, + pagination: SPORTMONKS_PAGINATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/sportmonks_odds/index.ts b/apps/sim/tools/sportmonks_odds/index.ts index ec81b21556e..105a5f151c0 100644 --- a/apps/sim/tools/sportmonks_odds/index.ts +++ b/apps/sim/tools/sportmonks_odds/index.ts @@ -1,8 +1,25 @@ +export { sportmonksOddsGetAllHistoricalOddsTool } from './get_all_historical_odds' +export { sportmonksOddsGetAllInplayOddsTool } from './get_all_inplay_odds' +export { sportmonksOddsGetAllPreMatchOddsTool } from './get_all_pre_match_odds' +export { sportmonksOddsGetAllPremiumOddsTool } from './get_all_premium_odds' export { sportmonksOddsGetBookmakerTool } from './get_bookmaker' +export { sportmonksOddsGetBookmakerEventIdsByFixtureTool } from './get_bookmaker_event_ids_by_fixture' export { sportmonksOddsGetBookmakersTool } from './get_bookmakers' +export { sportmonksOddsGetBookmakersByFixtureTool } from './get_bookmakers_by_fixture' export { sportmonksOddsGetInplayOddsByFixtureTool } from './get_inplay_odds_by_fixture' +export { sportmonksOddsGetInplayOddsByFixtureAndBookmakerTool } from './get_inplay_odds_by_fixture_and_bookmaker' +export { sportmonksOddsGetInplayOddsByFixtureAndMarketTool } from './get_inplay_odds_by_fixture_and_market' +export { sportmonksOddsGetLastUpdatedInplayOddsTool } from './get_last_updated_inplay_odds' +export { sportmonksOddsGetLastUpdatedPreMatchOddsTool } from './get_last_updated_pre_match_odds' export { sportmonksOddsGetMarketTool } from './get_market' export { sportmonksOddsGetMarketsTool } from './get_markets' export { sportmonksOddsGetPreMatchOddsByFixtureTool } from './get_pre_match_odds_by_fixture' +export { sportmonksOddsGetPreMatchOddsByFixtureAndBookmakerTool } from './get_pre_match_odds_by_fixture_and_bookmaker' +export { sportmonksOddsGetPreMatchOddsByFixtureAndMarketTool } from './get_pre_match_odds_by_fixture_and_market' +export { sportmonksOddsGetPremiumOddsByFixtureTool } from './get_premium_odds_by_fixture' +export { sportmonksOddsGetPremiumOddsByFixtureAndBookmakerTool } from './get_premium_odds_by_fixture_and_bookmaker' +export { sportmonksOddsGetPremiumOddsByFixtureAndMarketTool } from './get_premium_odds_by_fixture_and_market' +export { sportmonksOddsGetUpdatedHistoricalOddsBetweenTool } from './get_updated_historical_odds_between' +export { sportmonksOddsGetUpdatedPremiumOddsBetweenTool } from './get_updated_premium_odds_between' export { sportmonksOddsSearchBookmakersTool } from './search_bookmakers' export { sportmonksOddsSearchMarketsTool } from './search_markets' diff --git a/apps/sim/tools/sportmonks_odds/search_bookmakers.ts b/apps/sim/tools/sportmonks_odds/search_bookmakers.ts index dc03a8fafff..80ac35f4059 100644 --- a/apps/sim/tools/sportmonks_odds/search_bookmakers.ts +++ b/apps/sim/tools/sportmonks_odds/search_bookmakers.ts @@ -61,6 +61,12 @@ export const sportmonksOddsSearchBookmakersTool: ToolConfig< visibility: 'user-or-llm', description: 'Page number to retrieve', }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, }, request: { diff --git a/apps/sim/tools/sportmonks_odds/search_markets.ts b/apps/sim/tools/sportmonks_odds/search_markets.ts index dd85ef536e7..a76c4393d20 100644 --- a/apps/sim/tools/sportmonks_odds/search_markets.ts +++ b/apps/sim/tools/sportmonks_odds/search_markets.ts @@ -61,6 +61,12 @@ export const sportmonksOddsSearchMarketsTool: ToolConfig< visibility: 'user-or-llm', description: 'Page number to retrieve', }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order direction (asc or desc)', + }, }, request: { diff --git a/apps/sim/tools/sportmonks_odds/types.ts b/apps/sim/tools/sportmonks_odds/types.ts index 90fbdadf7ab..4cb65784984 100644 --- a/apps/sim/tools/sportmonks_odds/types.ts +++ b/apps/sim/tools/sportmonks_odds/types.ts @@ -1,11 +1,22 @@ import type { OutputProperty } from '@/tools/types' /** - * Base URL for the Sportmonks Odds API v3. - * @see https://docs.sportmonks.com/v3/odds-api/getting-started/welcome + * Base URL for the shared Sportmonks Odds reference resources (bookmakers and + * markets). These live under the sport-agnostic `/v3/odds` path. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/endpoints/bookmakers/get-all-bookmakers + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/endpoints/markets/get-all-markets */ export const SPORTMONKS_ODDS_BASE_URL = 'https://api.sportmonks.com/v3/odds' +/** + * Base URL for the Sportmonks football odds feeds (pre-match and in-play odds by + * fixture). Unlike bookmakers/markets, these endpoints are sport-scoped and live + * under the `/v3/football/odds` path. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/endpoints/pre-match-odds/get-odds-by-fixture-id + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/endpoints/inplay-odds/get-odds-by-fixture-id + */ +export const SPORTMONKS_FOOTBALL_ODDS_BASE_URL = 'https://api.sportmonks.com/v3/football/odds' + /** * Output property definitions for a pre-match Odd object. * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/odd @@ -84,6 +95,12 @@ export const SPORTMONKS_ODD_PROPERTIES = { nullable: true, optional: true, }, + original_label: { + type: 'string', + description: 'Original handicap value of the odd (handicap markets)', + nullable: true, + optional: true, + }, } as const satisfies Record /** @@ -173,6 +190,129 @@ export const SPORTMONKS_INPLAY_ODD_PROPERTIES = { }, } as const satisfies Record +/** + * Output property definitions for a Premium Odd object. Premium odds carry + * created/updated timestamps and do not yet expose winning calculations. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/premium-odds-feed/premium-pre-match-odds/get-all-premium-odds + */ +export const SPORTMONKS_PREMIUM_ODD_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the odd' }, + fixture_id: { type: 'number', description: 'Fixture the odd belongs to' }, + market_id: { type: 'number', description: 'Market the odd belongs to' }, + bookmaker_id: { type: 'number', description: 'Bookmaker offering the odd' }, + label: { type: 'string', description: 'Outcome label', nullable: true }, + value: { type: 'string', description: 'Decimal odds value', nullable: true }, + name: { type: 'string', description: 'Outcome name', nullable: true }, + sort_order: { + type: 'number', + description: 'Sort order of the odd', + nullable: true, + optional: true, + }, + market_description: { + type: 'string', + description: 'Description of the market', + nullable: true, + optional: true, + }, + probability: { + type: 'string', + description: 'Implied probability (e.g. 29.85%)', + nullable: true, + optional: true, + }, + dp3: { + type: 'string', + description: 'Decimal odds to 3 decimal places', + nullable: true, + optional: true, + }, + fractional: { type: 'string', description: 'Fractional odds', nullable: true, optional: true }, + american: { + type: 'string', + description: 'American/moneyline odds', + nullable: true, + optional: true, + }, + stopped: { + type: 'boolean', + description: 'Whether the odd is stopped', + nullable: true, + optional: true, + }, + total: { + type: 'string', + description: 'Total line for over/under markets', + nullable: true, + optional: true, + }, + handicap: { + type: 'string', + description: 'Handicap line for handicap markets', + nullable: true, + optional: true, + }, + created_at: { + type: 'string', + description: 'Timestamp the odd was created (UTC)', + nullable: true, + optional: true, + }, + updated_at: { + type: 'string', + description: 'Timestamp the odd was last updated (UTC)', + nullable: true, + optional: true, + }, + latest_bookmaker_update: { + type: 'string', + description: "Bookmaker's own last-update timestamp (UTC)", + nullable: true, + optional: true, + }, +} as const satisfies Record + +/** + * Output property definitions for a Premium Odd history record. Each record is a + * historical value of a premium odd referenced by `odd_id`. + * @see https://docs.sportmonks.com/v3/endpoints-and-entities/endpoints/premium-odds-feed/premium-pre-match-odds/get-all-historical-odds + */ +export const SPORTMONKS_PREMIUM_ODD_HISTORY_PROPERTIES = { + id: { type: 'number', description: 'Unique id of the history record' }, + odd_id: { type: 'number', description: 'Premium odd this history record belongs to' }, + value: { + type: 'string', + description: 'Historical decimal odds value', + nullable: true, + optional: true, + }, + probability: { + type: 'string', + description: 'Implied probability at this point in time', + nullable: true, + optional: true, + }, + dp3: { + type: 'string', + description: 'Decimal odds to 3 decimal places', + nullable: true, + optional: true, + }, + fractional: { type: 'string', description: 'Fractional odds', nullable: true, optional: true }, + american: { + type: 'string', + description: 'American/moneyline odds', + nullable: true, + optional: true, + }, + bookmaker_update: { + type: 'string', + description: "Bookmaker's update timestamp for this record (UTC)", + nullable: true, + optional: true, + }, +} as const satisfies Record + /** * Output property definitions for a Bookmaker object. * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/bookmaker @@ -183,6 +323,29 @@ export const SPORTMONKS_BOOKMAKER_PROPERTIES = { logo: { type: 'string', description: 'Logo of the bookmaker', nullable: true, optional: true }, } as const satisfies Record +/** + * Output property definitions for a bookmaker event mapping record, returned by + * the "bookmaker event ids by fixture" endpoint. Maps a Sportmonks fixture to a + * bookmaker's own event id. + * @see https://docs.sportmonks.com/v3/odds-api/getting-started/endpoints/bookmakers/get-bookmaker-event-ids-by-fixture-id + */ +export const SPORTMONKS_BOOKMAKER_EVENT_PROPERTIES = { + fixture_id: { type: 'number', description: 'Sportmonks fixture id' }, + bookmaker_id: { type: 'number', description: 'Id of the bookmaker' }, + bookmaker_name: { + type: 'string', + description: 'Name of the bookmaker', + nullable: true, + optional: true, + }, + bookmaker_event_id: { + type: 'string', + description: "The fixture's event id at the bookmaker", + nullable: true, + optional: true, + }, +} as const satisfies Record + /** * Output property definitions for a Market object. * @see https://docs.sportmonks.com/v3/odds-api/getting-started/entities/market @@ -190,6 +353,12 @@ export const SPORTMONKS_BOOKMAKER_PROPERTIES = { export const SPORTMONKS_MARKET_PROPERTIES = { id: { type: 'number', description: 'Unique id of the market' }, name: { type: 'string', description: 'Name of the market' }, + developer_name: { + type: 'string', + description: 'Developer (machine-readable) name of the market', + nullable: true, + optional: true, + }, } as const satisfies Record export interface SportmonksOdd { @@ -211,6 +380,7 @@ export interface SportmonksOdd { total?: string | null handicap?: string | null participants?: string | null + original_label?: string | null } export interface SportmonksInplayOdd extends SportmonksOdd { @@ -218,13 +388,54 @@ export interface SportmonksInplayOdd extends SportmonksOdd { suspended?: boolean | null } +export interface SportmonksPremiumOdd { + id: number + fixture_id: number + market_id: number + bookmaker_id: number + label: string | null + value: string | null + name: string | null + sort_order?: number | null + market_description?: string | null + probability?: string | null + dp3?: string | null + fractional?: string | null + american?: string | null + stopped?: boolean | null + total?: string | null + handicap?: string | null + created_at?: string | null + updated_at?: string | null + latest_bookmaker_update?: string | null +} + +export interface SportmonksPremiumOddHistory { + id: number + odd_id: number + value?: string | null + probability?: string | null + dp3?: string | null + fractional?: string | null + american?: string | null + bookmaker_update?: string | null +} + export interface SportmonksBookmaker { id: number name: string logo?: string | null } +export interface SportmonksBookmakerEvent { + fixture_id: number + bookmaker_id: number + bookmaker_name?: string | null + bookmaker_event_id?: string | null +} + export interface SportmonksMarket { id: number name: string + developer_name?: string | null } From 63fdc472c172fe76514f298c253abbe298c04143 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 18 Jun 2026 15:15:12 -0700 Subject: [PATCH 04/16] improvement(block): table empty-state filter/sort builders + upsert conflict-column selection (#5123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci(migrations): fail dev schema push with an actionable error on rename/drop prompt `drizzle-kit push --force` only suppresses the data-loss confirm, not the rename-vs-drop disambiguation prompt. That prompt fires whenever a diff both adds and drops tables/columns at once (e.g. migration 0231 created sim_trigger_state while dropping the workspace_notification_* tables), and in CI it crashes with a bare "Interactive prompts require a TTY" stack trace. Catch that specific failure in the dev push step and emit a GitHub error annotation explaining the cause and the fix (drop the stale objects on the dev DB to match schema.ts — the same DROPs the versioned migration already applied to staging/prod), instead of leaving an opaque trace. Exit status is preserved either way. Co-Authored-By: Claude Fable 5 * improvement(tables): empty-state filter/sort builders + upsert conflict-column selection * improvement(tables): throw on ambiguous upsert instead of guessing the conflict column * Revert "ci(migrations): fail dev schema push with an actionable error on rename/drop prompt" This reverts commit 26264822690dc64ce81028b23d6a54bf60ab1493. * improvement(tables): unique-column picker for upsert + richer get-schema (counts, ids, live plan row limit) * fix(tables): honor OR boundary when skipping incomplete filter rows * fix(tables): source workspaceId for column selector from route context --------- Co-authored-by: Claude Fable 5 --- apps/sim/app/api/table/[tableId]/route.ts | 7 ++- .../filter-builder/filter-builder.tsx | 38 +++++++-------- .../components/sort-builder/sort-builder.tsx | 28 +++++++---- .../sub-block/hooks/use-selector-setup.ts | 3 ++ .../editor/components/sub-block/sub-block.tsx | 1 + apps/sim/blocks/blocks.test.ts | 1 + apps/sim/blocks/blocks/table.ts | 48 +++++++++++++++++-- apps/sim/hooks/queries/tables.ts | 12 +++++ .../selectors/providers/sim/selectors.ts | 33 ++++++++++++- apps/sim/hooks/selectors/types.ts | 2 + .../sim/lib/table/query-builder/converters.ts | 10 +++- apps/sim/lib/table/rows/service.ts | 2 +- apps/sim/lib/workflows/subblocks/context.ts | 1 + apps/sim/tools/table/get_schema.ts | 22 ++++++++- apps/sim/tools/table/types.ts | 5 ++ apps/sim/tools/table/upsert_row.ts | 8 ++++ packages/workflow-types/src/blocks.ts | 1 + 17 files changed, 184 insertions(+), 38 deletions(-) diff --git a/apps/sim/app/api/table/[tableId]/route.ts b/apps/sim/app/api/table/[tableId]/route.ts index 0d185a74784..bd398867eeb 100644 --- a/apps/sim/app/api/table/[tableId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/route.ts @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { deleteTable, renameTable, TableConflictError, type TableSchema } from '@/lib/table' +import { getWorkspaceTableLimits } from '@/lib/table/billing' import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils' const logger = createLogger('TableDetailAPI') @@ -46,6 +47,10 @@ export const GET = withRouteHandler(async (request: NextRequest, { params }: Tab const schemaData = table.schema as TableSchema + // Source the row cap from the workspace's live plan, not the value stored on + // the table at creation time (which goes stale when the plan changes). + const { maxRowsPerTable } = await getWorkspaceTableLimits(table.workspaceId) + return NextResponse.json({ success: true, data: { @@ -59,7 +64,7 @@ export const GET = withRouteHandler(async (request: NextRequest, { params }: Tab }, metadata: table.metadata ?? null, rowCount: table.rowCount, - maxRows: table.maxRows, + maxRows: maxRowsPerTable, createdAt: table.createdAt instanceof Date ? table.createdAt.toISOString() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx index 6d43669249e..52fc56e7a41 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx @@ -1,8 +1,8 @@ 'use client' import { useCallback, useMemo } from 'react' -import { generateId } from '@sim/utils/id' -import type { ComboboxOption } from '@/components/emcn' +import { Plus } from 'lucide-react' +import { Button } from '@/components/emcn' import { useTableColumns } from '@/lib/table/hooks' import type { FilterRule } from '@/lib/table/query-builder/constants' import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder' @@ -22,15 +22,6 @@ interface FilterBuilderProps { tableIdSubBlockId?: string } -const createDefaultRule = (columns: ComboboxOption[]): FilterRule => ({ - id: generateId(), - logicalOperator: 'and', - column: columns[0]?.value || '', - operator: 'eq', - value: '', - collapsed: false, -}) - /** Visual builder for table filter rules in workflow blocks. */ export function FilterBuilder({ blockId, @@ -52,8 +43,7 @@ export function FilterBuilder({ }, [propColumns, dynamicColumns]) const value = isPreview ? previewValue : storeValue - const rules: FilterRule[] = - Array.isArray(value) && value.length > 0 ? value : [createDefaultRule(columns)] + const rules: FilterRule[] = Array.isArray(value) ? value : [] const isReadOnly = isPreview || disabled const { comparisonOptions, logicalOptions, addRule, removeRule, updateRule } = useFilterBuilder({ @@ -86,15 +76,25 @@ export function FilterBuilder({ const handleRemoveRule = useCallback( (id: string) => { if (isReadOnly) return - if (rules.length === 1) { - setStoreValue([createDefaultRule(columns)]) - } else { - removeRule(id) - } + removeRule(id) }, - [isReadOnly, rules, columns, setStoreValue, removeRule] + [isReadOnly, removeRule] ) + if (rules.length === 0) { + if (isReadOnly) return null + return ( + + ) + } + return (
{rules.map((rule, index) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx index 38a608841a2..6095ada1df3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx @@ -2,7 +2,8 @@ import { useCallback, useMemo } from 'react' import { generateId } from '@sim/utils/id' -import type { ComboboxOption } from '@/components/emcn' +import { Plus } from 'lucide-react' +import { Button, type ComboboxOption } from '@/components/emcn' import { useTableColumns } from '@/lib/table/hooks' import { SORT_DIRECTIONS, type SortRule } from '@/lib/table/query-builder/constants' import { useCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value' @@ -51,8 +52,7 @@ export function SortBuilder({ ) const value = isPreview ? previewValue : storeValue - const rules: SortRule[] = - Array.isArray(value) && value.length > 0 ? value : [createDefaultRule(columns)] + const rules: SortRule[] = Array.isArray(value) ? value : [] const isReadOnly = isPreview || disabled const addRule = useCallback(() => { @@ -63,13 +63,9 @@ export function SortBuilder({ const removeRule = useCallback( (id: string) => { if (isReadOnly) return - if (rules.length === 1) { - setStoreValue([createDefaultRule(columns)]) - } else { - setStoreValue(rules.filter((r) => r.id !== id)) - } + setStoreValue(rules.filter((r) => r.id !== id)) }, - [isReadOnly, rules, columns, setStoreValue] + [isReadOnly, rules, setStoreValue] ) const updateRule = useCallback( @@ -88,6 +84,20 @@ export function SortBuilder({ [isReadOnly, rules, setStoreValue] ) + if (rules.length === 0) { + if (isReadOnly) return null + return ( + + ) + } + return (
{rules.map((rule, index) => ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index 555496c899a..9e1e101f6f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -31,6 +31,7 @@ export function useSelectorSetup( const params = useParams() const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) const workflowId = (params?.workflowId as string) || activeWorkflowId || '' + const workspaceId = (params?.workspaceId as string) || '' const { data: envVariables = {} } = usePersonalEnvironment() @@ -63,6 +64,7 @@ export function useSelectorSetup( const selectorContext = useMemo(() => { const context: SelectorContext = { workflowId, + workspaceId: workspaceId || undefined, mimeType: subBlock.mimeType, } @@ -87,6 +89,7 @@ export function useSelectorSetup( resolvedDependencyValues, canonicalIndex, workflowId, + workspaceId, subBlock.mimeType, impersonateUserEmail, ]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index a27157e1265..71c94d10337 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -929,6 +929,7 @@ function SubBlockComponent({ case 'file-selector': case 'sheet-selector': case 'project-selector': + case 'column-selector': return ( { 'text', 'router-input', 'table-selector', + 'column-selector', 'filter-builder', 'sort-builder', 'skill-input', diff --git a/apps/sim/blocks/blocks/table.ts b/apps/sim/blocks/blocks/table.ts index 80dba4a7015..281bcd077e7 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -58,6 +58,7 @@ interface TableBlockParams { sortBuilder?: unknown bulkFilterMode?: string bulkFilterBuilder?: unknown + conflictColumn?: string } /** Normalized params after parsing, ready for tool request body */ @@ -70,6 +71,7 @@ interface ParsedParams { sort?: unknown limit?: number offset?: number + conflictTarget?: string } /** Transforms raw block params into tool request params for each operation */ @@ -82,6 +84,7 @@ const paramTransformers: Record ParsedPara upsert_row: (params) => ({ tableId: params.tableId, data: parseJSON(params.data, 'Row Data'), + conflictTarget: params.conflictColumn || undefined, }), batch_insert_rows: (params) => ({ @@ -275,6 +278,30 @@ Return ONLY the data JSON:`, }, }, + // Upsert - which unique column to match on (required when 2+ unique columns) + // Basic: pick a unique column. Advanced: enter the column id directly. + { + id: 'conflictColumnSelector', + title: 'Conflict Column', + type: 'column-selector', + canonicalParamId: 'conflictColumn', + mode: 'basic', + selectorKey: 'table.columns', + placeholder: 'Select a unique column', + dependsOn: ['tableSelector'], + condition: { field: 'operation', value: 'upsert_row' }, + }, + { + id: 'manualConflictColumn', + title: 'Conflict Column', + type: 'short-input', + canonicalParamId: 'conflictColumn', + mode: 'advanced', + placeholder: 'Enter the column id', + dependsOn: ['tableId'], + condition: { field: 'operation', value: 'upsert_row' }, + }, + // Batch Insert - multiple rows { id: 'rows', @@ -631,6 +658,11 @@ Return ONLY the sort JSON:`, sortBuilder: { type: 'json', description: 'Visual sort builder conditions' }, sort: { type: 'json', description: 'Sort order (JSON)' }, offset: { type: 'number', description: 'Query result offset' }, + conflictColumn: { + type: 'string', + description: + 'Unique column to match on for upsert (required if the table has multiple unique columns)', + }, }, outputs: { @@ -655,8 +687,8 @@ Return ONLY the sort JSON:`, }, rowCount: { type: 'number', - description: 'Number of rows returned', - condition: { field: 'operation', value: 'query_rows' }, + description: 'Rows returned (query) or total rows in the table (get schema)', + condition: { field: 'operation', value: ['query_rows', 'get_schema'] }, }, totalCount: { type: 'number', @@ -695,7 +727,17 @@ Return ONLY the sort JSON:`, }, columns: { type: 'array', - description: 'Column definitions', + description: 'Column definitions (each includes its stable id)', + condition: { field: 'operation', value: 'get_schema' }, + }, + columnCount: { + type: 'number', + description: 'Number of columns', + condition: { field: 'operation', value: 'get_schema' }, + }, + maxRows: { + type: 'number', + description: "Max rows per table for the workspace's plan", condition: { field: 'operation', value: 'get_schema' }, }, message: { type: 'string', description: 'Operation status message' }, diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 5825ebd6c47..4bc2a19ce33 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -267,6 +267,18 @@ export function useTable(workspaceId: string | undefined, tableId: string | unde }) } +/** + * Shared table-detail query options so non-component callers (e.g. selector + * providers) can `ensureQueryData` the same cache entry `useTable` populates. + */ +export function getTableDetailQueryOptions(workspaceId: string, tableId: string) { + return { + queryKey: tableKeys.detail(tableId), + queryFn: ({ signal }: { signal?: AbortSignal }) => fetchTable(workspaceId, tableId, signal), + staleTime: 30 * 1000, + } +} + export interface TableRunState { dispatches: ActiveDispatch[] runningCellCount: number diff --git a/apps/sim/hooks/selectors/providers/sim/selectors.ts b/apps/sim/hooks/selectors/providers/sim/selectors.ts index 45881d47e7c..1c7784cd982 100644 --- a/apps/sim/hooks/selectors/providers/sim/selectors.ts +++ b/apps/sim/hooks/selectors/providers/sim/selectors.ts @@ -1,4 +1,6 @@ +import { getColumnId } from '@/lib/table/column-keys' import { getQueryClient } from '@/app/_shell/providers/get-query-client' +import { getTableDetailQueryOptions } from '@/hooks/queries/tables' import { getFolderMap } from '@/hooks/queries/utils/folder-cache' import { getFolderPath } from '@/hooks/queries/utils/folder-tree' import { getWorkflowById, getWorkflows } from '@/hooks/queries/utils/workflow-cache' @@ -85,4 +87,33 @@ export const simSelectors = { } }, }, -} satisfies Record, SelectorDefinition> + 'table.columns': { + key: 'table.columns', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context, search }: SelectorQueryArgs) => [ + ...selectorKeys.all, + 'table.columns', + context.workspaceId ?? 'none', + context.tableId ?? 'none', + search ?? '', + ], + enabled: ({ context }) => Boolean(context.workspaceId && context.tableId), + fetchList: async ({ context }: SelectorQueryArgs): Promise => { + if (!context.workspaceId || !context.tableId) return [] + const table = await getQueryClient().ensureQueryData( + getTableDetailQueryOptions(context.workspaceId, context.tableId) + ) + return (table.schema?.columns ?? []) + .filter((col) => col.unique) + .map((col) => ({ id: getColumnId(col), label: col.name })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs): Promise => { + if (!detailId || !context.workspaceId || !context.tableId) return null + const table = await getQueryClient().ensureQueryData( + getTableDetailQueryOptions(context.workspaceId, context.tableId) + ) + const col = (table.schema?.columns ?? []).find((c) => getColumnId(c) === detailId) + return col ? { id: getColumnId(col), label: col.name } : null + }, + }, +} satisfies Record, SelectorDefinition> diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index 48af9cfda80..0b26fdd130f 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -56,6 +56,7 @@ export type SelectorKey = | 'monday.boards' | 'monday.groups' | 'sim.workflows' + | 'table.columns' export interface SelectorOption { id: string @@ -91,6 +92,7 @@ export interface SelectorContext { awsRegion?: string logGroupName?: string mcpServerId?: string + tableId?: string } export interface SelectorQueryArgs { diff --git a/apps/sim/lib/table/query-builder/converters.ts b/apps/sim/lib/table/query-builder/converters.ts index 4dc29f4b0ae..4d6b5f8753e 100644 --- a/apps/sim/lib/table/query-builder/converters.ts +++ b/apps/sim/lib/table/query-builder/converters.ts @@ -21,14 +21,20 @@ export function filterRulesToFilter(rules: FilterRule[]): Filter | null { let currentGroup: Filter = {} for (const rule of rules) { + // Honor the OR boundary before skipping incomplete rows, so an incomplete + // `or` row between two valid conditions still starts a new group. const isOr = rule.logicalOperator === 'or' - const ruleValue = toRuleValue(rule.operator, rule.value) - if (isOr && Object.keys(currentGroup).length > 0) { orGroups.push({ ...currentGroup }) currentGroup = {} } + // Skip incomplete rows (no column selected) so a blank builder row never + // serializes to a `{ '': ... }` predicate. The OR boundary above is still + // applied; the row just contributes no condition. + if (!rule.column) continue + + const ruleValue = toRuleValue(rule.operator, rule.value) const existing = currentGroup[rule.column] currentGroup[rule.column] = existing === undefined diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts index 5cc156dbfa9..7f9878a7e16 100644 --- a/apps/sim/lib/table/rows/service.ts +++ b/apps/sim/lib/table/rows/service.ts @@ -516,7 +516,7 @@ export async function upsertRow( targetColumnKey = getColumnId(uniqueColumns[0]) } else { throw new Error( - `Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify conflictTarget to indicate which column to match on.` + `Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify a conflict column to indicate which one to match on.` ) } diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index fd32f9d696a..a1b6a9076bd 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -28,6 +28,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'awsSecretAccessKey', 'awsRegion', 'logGroupName', + 'tableId', ]) /** diff --git a/apps/sim/tools/table/get_schema.ts b/apps/sim/tools/table/get_schema.ts index 2bea5755af7..7f96f0dd065 100644 --- a/apps/sim/tools/table/get_schema.ts +++ b/apps/sim/tools/table/get_schema.ts @@ -1,3 +1,5 @@ +import { getColumnId } from '@/lib/table/column-keys' +import type { ColumnDefinition } from '@/lib/table/types' import type { TableGetSchemaParams, TableGetSchemaResponse } from '@/tools/table/types' import type { ToolConfig } from '@/tools/types' @@ -35,11 +37,21 @@ export const tableGetSchemaTool: ToolConfig ({ ...col, id: getColumnId(col) })) + return { success: true, output: { name: data.table.name, - columns: data.table.schema.columns, + columns, + columnCount: columns.length, + rowCount: data.table.rowCount ?? 0, + maxRows: data.table.maxRows ?? 0, message: data.message || 'Schema retrieved successfully', }, } @@ -48,7 +60,13 @@ export const tableGetSchemaTool: ToolConfig Date: Thu, 18 Jun 2026 15:48:02 -0700 Subject: [PATCH 05/16] improvement(workspaces): auto-add without invite if part of organization (#5132) * feat(workspaces): auto-add without invite if part of organization * reverse feature flag hardcoding * address comments * improve ux for org invite modal --- .../[id]/invitations/route.test.ts | 142 +++++++++-- .../organizations/[id]/invitations/route.ts | 201 ++++++++------- .../api/workspaces/invitations/batch/route.ts | 10 +- .../organization-invite-modal.tsx | 38 ++- .../organization-member-lists.tsx | 42 +++- .../components/invite-modal/invite-modal.tsx | 22 +- .../components/emails/invitations/index.ts | 1 + .../invitations/workspace-added-email.tsx | 44 ++++ apps/sim/components/emails/render.ts | 15 ++ apps/sim/components/emails/subjects.ts | 3 + apps/sim/hooks/queries/invitations.ts | 15 +- apps/sim/hooks/queries/organization.ts | 24 +- apps/sim/lib/api/contracts/invitations.ts | 1 + apps/sim/lib/api/contracts/organization.ts | 2 + apps/sim/lib/core/telemetry.ts | 18 ++ apps/sim/lib/invitations/core.ts | 4 +- apps/sim/lib/invitations/direct-grant.test.ts | 214 ++++++++++++++++ apps/sim/lib/invitations/direct-grant.ts | 235 ++++++++++++++++++ apps/sim/lib/invitations/send.ts | 42 +++- .../invitations/workspace-invitations.test.ts | 231 +++++++++++++++++ .../lib/invitations/workspace-invitations.ts | 66 ++++- apps/sim/lib/posthog/events.ts | 5 + packages/audit/src/types.ts | 1 + packages/testing/src/mocks/audit.mock.ts | 1 + 24 files changed, 1226 insertions(+), 151 deletions(-) create mode 100644 apps/sim/components/emails/invitations/workspace-added-email.tsx create mode 100644 apps/sim/lib/invitations/direct-grant.test.ts create mode 100644 apps/sim/lib/invitations/direct-grant.ts create mode 100644 apps/sim/lib/invitations/workspace-invitations.test.ts diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.test.ts b/apps/sim/app/api/organizations/[id]/invitations/route.test.ts index c069e3c950e..43f4ff6ce8b 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.test.ts @@ -12,6 +12,7 @@ const { mockCreatePendingInvitation, mockSendInvitationEmail, mockCancelPendingInvitation, + mockGrantWorkspaceAccessDirectly, } = vi.hoisted(() => ({ mockDbState: { selectResults: [] as any[], @@ -22,6 +23,7 @@ const { mockCreatePendingInvitation: vi.fn(), mockSendInvitationEmail: vi.fn(), mockCancelPendingInvitation: vi.fn(), + mockGrantWorkspaceAccessDirectly: vi.fn(), })) function createSelectChain() { @@ -115,6 +117,10 @@ vi.mock('@/lib/invitations/send', () => ({ cancelPendingInvitation: mockCancelPendingInvitation, })) +vi.mock('@/lib/invitations/direct-grant', () => ({ + grantWorkspaceAccessDirectly: mockGrantWorkspaceAccessDirectly, +})) + vi.mock('@/lib/messaging/email/validation', () => ({ quickValidateEmail: vi.fn((email: string) => ({ isValid: email.includes('@') })), })) @@ -151,6 +157,7 @@ describe('POST /api/organizations/[id]/invitations', () => { expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }) mockSendInvitationEmail.mockResolvedValue({ success: true }) + mockGrantWorkspaceAccessDirectly.mockResolvedValue({ outcome: 'added', permission: 'write' }) }) it('creates a unified invitation and sends a single email', async () => { @@ -191,15 +198,15 @@ describe('POST /api/organizations/[id]/invitations', () => { expect(mockCancelPendingInvitation).not.toHaveBeenCalled() }) - it('sends a workspace invitation to an existing member for selected workspaces they lack', async () => { + it('adds an existing member directly to selected workspaces they lack (no invitation/email)', async () => { mockGetSession.mockResolvedValue( createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' }) ) mockDbState.selectResults = [ [{ role: 'owner' }], [{ name: 'Org One' }], - [{ id: 'ws-1', organizationId: 'org-1', workspaceMode: 'organization' }], - [{ id: 'ws-2', organizationId: 'org-1', workspaceMode: 'organization' }], + [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }], + [{ id: 'ws-2', name: 'Workspace 2', organizationId: 'org-1', workspaceMode: 'organization' }], [{ userId: 'user-2', userEmail: 'member@example.com' }], [], [{ userId: 'user-2', workspaceId: 'ws-1' }], @@ -224,30 +231,111 @@ describe('POST /api/organizations/[id]/invitations', () => { ) expect(response.status).toBe(200) - expect(mockCreatePendingInvitation).toHaveBeenCalledTimes(1) - expect(mockCreatePendingInvitation).toHaveBeenCalledWith( + expect(mockCreatePendingInvitation).not.toHaveBeenCalled() + expect(mockSendInvitationEmail).not.toHaveBeenCalled() + expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledTimes(1) + expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledWith( expect.objectContaining({ - kind: 'workspace', + userId: 'user-2', email: 'member@example.com', + workspaceId: 'ws-2', + permission: 'write', organizationId: 'org-1', - membershipIntent: 'internal', - grants: [{ workspaceId: 'ws-2', permission: 'write' }], - }) - ) - expect(mockSendInvitationEmail).toHaveBeenCalledWith( - expect.objectContaining({ - kind: 'workspace', - email: 'member@example.com', - grants: [{ workspaceId: 'ws-2', permission: 'write' }], }) ) const body = await response.json() - expect(body.data.invitationsSent).toBe(1) - expect(body.data.invitedEmails).toEqual(['member@example.com']) + expect(body.data.invitationsSent).toBe(0) + expect(body.data.directlyAdded).toEqual(['member@example.com']) + expect(body.data.directlyAddedCount).toBe(1) expect(body.data.existingMembers).toEqual([]) }) + it('reports a partially-failed member only as added, never in both buckets', async () => { + mockGetSession.mockResolvedValue( + createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' }) + ) + // First grant succeeds, second throws (e.g. transient DB error). + mockGrantWorkspaceAccessDirectly + .mockResolvedValueOnce({ outcome: 'added', permission: 'write' }) + .mockRejectedValueOnce(new Error('db blip')) + mockDbState.selectResults = [ + [{ role: 'owner' }], + [{ name: 'Org One' }], + [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }], + [{ id: 'ws-2', name: 'Workspace 2', organizationId: 'org-1', workspaceMode: 'organization' }], + [{ userId: 'user-2', userEmail: 'member@example.com' }], + [], + [], + [], + [{ name: 'Owner', email: 'owner@example.com' }], + ] + + const response = await POST( + createMockRequest( + 'POST', + { + emails: ['member@example.com'], + workspaceInvitations: [ + { workspaceId: 'ws-1', permission: 'write' }, + { workspaceId: 'ws-2', permission: 'write' }, + ], + }, + {}, + 'http://localhost/api/organizations/org-1/invitations?batch=true' + ), + { params: Promise.resolve({ id: 'org-1' }) } + ) + + expect(response.status).toBe(200) + expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledTimes(2) + const body = await response.json() + expect(body.data.directlyAdded).toEqual(['member@example.com']) + expect(body.data.failedInvitations).toEqual([]) + }) + + it('returns 207 with both successes and failures when one member is added and another fails', async () => { + mockGetSession.mockResolvedValue( + createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' }) + ) + mockGrantWorkspaceAccessDirectly + .mockResolvedValueOnce({ outcome: 'added', permission: 'write' }) + .mockRejectedValueOnce(new Error('db blip')) + mockDbState.selectResults = [ + [{ role: 'owner' }], + [{ name: 'Org One' }], + [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }], + [ + { userId: 'user-a', userEmail: 'a@example.com' }, + { userId: 'user-b', userEmail: 'b@example.com' }, + ], + [], + [], + [], + [{ name: 'Owner', email: 'owner@example.com' }], + ] + + const response = await POST( + createMockRequest( + 'POST', + { + emails: ['a@example.com', 'b@example.com'], + workspaceInvitations: [{ workspaceId: 'ws-1', permission: 'write' }], + }, + {}, + 'http://localhost/api/organizations/org-1/invitations?batch=true' + ), + { params: Promise.resolve({ id: 'org-1' }) } + ) + + expect(response.status).toBe(207) + const body = await response.json() + expect(body.success).toBe(false) + expect(body.data.directlyAdded).toEqual(['a@example.com']) + expect(body.data.directlyAddedCount).toBe(1) + expect(body.data.failedInvitations).toEqual([{ email: 'b@example.com', error: 'db blip' }]) + }) + it('returns 400 when an existing member already has access to every selected workspace', async () => { mockGetSession.mockResolvedValue( createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' }) @@ -281,14 +369,14 @@ describe('POST /api/organizations/[id]/invitations', () => { expect(mockCreatePendingInvitation).not.toHaveBeenCalled() }) - it('invites new emails to the organization and existing members to workspaces in one batch', async () => { + it('invites new emails to the organization and adds existing members to workspaces in one batch', async () => { mockGetSession.mockResolvedValue( createSession({ userId: 'user-1', email: 'owner@example.com', name: 'Owner' }) ) mockDbState.selectResults = [ [{ role: 'owner' }], [{ name: 'Org One' }], - [{ id: 'ws-1', organizationId: 'org-1', workspaceMode: 'organization' }], + [{ id: 'ws-1', name: 'Workspace 1', organizationId: 'org-1', workspaceMode: 'organization' }], [{ userId: 'user-2', userEmail: 'member@example.com' }], [], [], @@ -310,7 +398,7 @@ describe('POST /api/organizations/[id]/invitations', () => { ) expect(response.status).toBe(200) - expect(mockCreatePendingInvitation).toHaveBeenCalledTimes(2) + expect(mockCreatePendingInvitation).toHaveBeenCalledTimes(1) expect(mockCreatePendingInvitation).toHaveBeenCalledWith( expect.objectContaining({ kind: 'organization', @@ -318,17 +406,21 @@ describe('POST /api/organizations/[id]/invitations', () => { grants: [{ workspaceId: 'ws-1', permission: 'read' }], }) ) - expect(mockCreatePendingInvitation).toHaveBeenCalledWith( + expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledTimes(1) + expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledWith( expect.objectContaining({ - kind: 'workspace', + userId: 'user-2', email: 'member@example.com', - grants: [{ workspaceId: 'ws-1', permission: 'read' }], + workspaceId: 'ws-1', + permission: 'read', }) ) const body = await response.json() - expect(body.data.invitationsSent).toBe(2) - expect(body.data.invitedEmails).toEqual(['new@example.com', 'member@example.com']) + expect(body.data.invitationsSent).toBe(1) + expect(body.data.invitedEmails).toEqual(['new@example.com']) + expect(body.data.directlyAdded).toEqual(['member@example.com']) + expect(body.data.directlyAddedCount).toBe(1) }) it('still rejects existing members on the non-batch organization invite path', async () => { diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index de6bee05b77..afbd00e78ca 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -26,6 +26,7 @@ import { validateSeatAvailability, } from '@/lib/billing/validation/seat-management' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { grantWorkspaceAccessDirectly } from '@/lib/invitations/direct-grant' import { cancelPendingInvitation, createPendingInvitation, @@ -188,6 +189,7 @@ export const POST = withRouteHandler( } const validGrants: WorkspaceGrantPayload[] = [] + const workspaceNameById = new Map() if (isBatch) { if (!Array.isArray(workspaceInvitations) || workspaceInvitations.length === 0) { return NextResponse.json( @@ -214,6 +216,7 @@ export const POST = withRouteHandler( const [workspaceEntry] = await db .select({ id: workspace.id, + name: workspace.name, organizationId: workspace.organizationId, workspaceMode: workspace.workspaceMode, }) @@ -241,6 +244,7 @@ export const POST = withRouteHandler( await validateInvitationsAllowed(session.user.id, wsInvitation.workspaceId) + workspaceNameById.set(workspaceEntry.id, workspaceEntry.name) validGrants.push({ workspaceId: wsInvitation.workspaceId, permission: wsInvitation.permission, @@ -422,65 +426,45 @@ export const POST = withRouteHandler( .limit(1) const inviterName = inviterRow?.name || inviterRow?.email || 'A user' + const failedInvitations: Array<{ email: string; error: string }> = [] + /** - * Organization invitations (new emails, all selected grants) and - * workspace invitations (existing members, only the grants they lack) - * share one create/send/rollback pipeline; they differ only in `kind`, - * grants, and audit treatment. + * Brand-new emails receive an organization invitation (with all selected + * workspace grants) that still requires acceptance — accepting is what + * joins them to the org and consumes a seat. */ - const pendingSends = [ - ...emailsToInvite.map((email) => ({ - kind: 'organization' as const, - email, - grants: validGrants, - })), - ...memberWorkspaceInvites.map((memberInvite) => ({ - kind: 'workspace' as const, - email: memberInvite.email, - grants: memberInvite.grants, - })), - ] - - const sentInvitations: Array<{ - id: string - email: string - kind: 'organization' | 'workspace' - workspaceIds: string[] - }> = [] - const failedInvitations: Array<{ email: string; error: string }> = [] + const sentInvitations: Array<{ id: string; email: string; workspaceIds: string[] }> = [] - for (const send of pendingSends) { - const sendRole = send.kind === 'organization' ? role : 'member' + for (const email of emailsToInvite) { try { const { invitationId, token } = await createPendingInvitation({ - kind: send.kind, - email: send.email, + kind: 'organization', + email, inviterId: session.user.id, organizationId, membershipIntent: 'internal', - role: sendRole, - grants: send.grants, + role, + grants: validGrants, }) const emailResult = await sendInvitationEmail({ invitationId, token, - kind: send.kind, - email: send.email, + kind: 'organization', + email, inviterName, organizationId, - organizationRole: sendRole, - grants: send.grants, + organizationRole: role, + grants: validGrants, }) if (!emailResult.success) { logger.error('Failed to send invitation email', { - kind: send.kind, - email: send.email, + email, error: emailResult.error, }) failedInvitations.push({ - email: send.email, + email, error: emailResult.error || 'Unknown email delivery error', }) await cancelPendingInvitation(invitationId) @@ -489,76 +473,98 @@ export const POST = withRouteHandler( sentInvitations.push({ id: invitationId, - email: send.email, - kind: send.kind, - workspaceIds: send.grants.map((grant) => grant.workspaceId), + email, + workspaceIds: validGrants.map((grant) => grant.workspaceId), }) } catch (creationError) { logger.error('Failed to create invitation', { - kind: send.kind, - email: send.email, + email, error: creationError, }) failedInvitations.push({ - email: send.email, + email, error: getErrorMessage(creationError, 'Failed to create invitation'), }) } } - for (const inv of sentInvitations) { - if (inv.kind === 'organization') { - recordAudit({ - workspaceId: null, - actorId: session.user.id, - action: AuditAction.ORG_INVITATION_CREATED, - resourceType: AuditResourceType.ORGANIZATION, - resourceId: organizationId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - resourceName: organizationEntry.name, - description: `Invited ${inv.email} to organization as ${role}`, - metadata: { - invitationId: inv.id, - targetEmail: inv.email, - targetRole: role, - isBatch, - workspaceGrantCount: validGrants.length, - enforcedFixedSeats: enforceFixedSeats, - plan: orgSubscription?.plan ?? null, - }, - request, - }) - continue + /** + * Existing organization members are granted workspace access directly — + * no invitation, no acceptance step. They are already in the org, so no + * seat is consumed. The grant is idempotent and upgrades lower access. + */ + const directlyAdded: string[] = [] + + for (const memberInvite of memberWorkspaceInvites) { + const memberUserId = memberUserIdByEmail.get(memberInvite.email) + if (!memberUserId) continue + + let addedAny = false + let lastGrantError: string | null = null + for (const grant of memberInvite.grants) { + try { + const grantResult = await grantWorkspaceAccessDirectly({ + userId: memberUserId, + email: memberInvite.email, + workspaceId: grant.workspaceId, + workspaceName: workspaceNameById.get(grant.workspaceId) ?? 'a workspace', + permission: grant.permission, + organizationId, + actorId: session.user.id, + actorName: inviterName, + actorEmail: session.user.email, + request, + }) + + if (grantResult.outcome === 'added') addedAny = true + } catch (grantError) { + logger.error('Failed to grant workspace access directly', { + email: memberInvite.email, + workspaceId: grant.workspaceId, + error: grantError, + }) + lastGrantError = getErrorMessage(grantError, 'Failed to add member to workspace') + } } - for (const workspaceId of inv.workspaceIds) { - recordAudit({ - workspaceId, - actorId: session.user.id, - action: AuditAction.MEMBER_INVITED, - resourceType: AuditResourceType.WORKSPACE, - resourceId: workspaceId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - resourceName: inv.email, - description: `Invited existing organization member ${inv.email} to workspace`, - metadata: { - invitationId: inv.id, - targetEmail: inv.email, - organizationId, - isBatch, - }, - request, - }) + if (addedAny) { + directlyAdded.push(memberInvite.email) + } else if (lastGrantError) { + failedInvitations.push({ email: memberInvite.email, error: lastGrantError }) } } - const sentOrgInvitations = sentInvitations.filter((inv) => inv.kind === 'organization') + for (const inv of sentInvitations) { + recordAudit({ + workspaceId: null, + actorId: session.user.id, + action: AuditAction.ORG_INVITATION_CREATED, + resourceType: AuditResourceType.ORGANIZATION, + resourceId: organizationId, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: organizationEntry.name, + description: `Invited ${inv.email} to organization as ${role}`, + metadata: { + invitationId: inv.id, + targetEmail: inv.email, + targetRole: role, + isBatch, + workspaceGrantCount: validGrants.length, + enforcedFixedSeats: enforceFixedSeats, + plan: orgSubscription?.plan ?? null, + }, + request, + }) + } + const totalInvitationsSent = sentInvitations.length + const totalSucceeded = totalInvitationsSent + directlyAdded.length const responseData = { invitationsSent: totalInvitationsSent, invitedEmails: sentInvitations.map((inv) => inv.email), + directlyAdded, + directlyAddedCount: directlyAdded.length, failedInvitations, existingMembers: membersAlreadyCovered, pendingInvitations: processedEmails.filter( @@ -571,20 +577,25 @@ export const POST = withRouteHandler( ...(seatValidation ? { seatInfo: { - seatsUsed: seatValidation.currentSeats + sentOrgInvitations.length, + seatsUsed: seatValidation.currentSeats + totalInvitationsSent, maxSeats: seatValidation.maxSeats, - availableSeats: seatValidation.availableSeats - sentOrgInvitations.length, + availableSeats: seatValidation.availableSeats - totalInvitationsSent, }, } : {}), } - if (failedInvitations.length > 0 && totalInvitationsSent === 0) { + const summaryParts: string[] = [] + if (totalInvitationsSent > 0) summaryParts.push(`${totalInvitationsSent} invitation(s) sent`) + if (directlyAdded.length > 0) summaryParts.push(`${directlyAdded.length} member(s) added`) + const summary = summaryParts.join(', ') + + if (failedInvitations.length > 0 && totalSucceeded === 0) { return NextResponse.json( { success: false, - error: 'Failed to send invitation emails.', - message: 'No invitation emails could be delivered.', + error: 'Failed to send invitations.', + message: 'No invitations could be delivered.', data: responseData, }, { status: 502 } @@ -595,8 +606,8 @@ export const POST = withRouteHandler( return NextResponse.json( { success: false, - error: 'Some invitation emails failed to send.', - message: `${totalInvitationsSent} invitation(s) sent, ${failedInvitations.length} failed`, + error: 'Some invitations failed.', + message: `${summary}, ${failedInvitations.length} failed`, data: responseData, }, { status: 207 } @@ -605,7 +616,7 @@ export const POST = withRouteHandler( return NextResponse.json({ success: true, - message: `${totalInvitationsSent} invitation(s) sent successfully`, + message: `${summary || 'No changes'} successfully`, data: responseData, }) } catch (error) { diff --git a/apps/sim/app/api/workspaces/invitations/batch/route.ts b/apps/sim/app/api/workspaces/invitations/batch/route.ts index 02dc504458a..391a1cb8952 100644 --- a/apps/sim/app/api/workspaces/invitations/batch/route.ts +++ b/apps/sim/app/api/workspaces/invitations/batch/route.ts @@ -61,6 +61,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { }) const successful: string[] = [] + const added: string[] = [] const failed: BatchInvitationFailure[] = [] const invitations: WorkspaceInvitationResult[] = [] const seenEmails = new Set() @@ -83,7 +84,13 @@ export const POST = withRouteHandler(async (req: NextRequest) => { permission: item.permission, request: req, }) - successful.push(invitation.email) + if (invitation.instantAdd) { + // Only report an actual insertion; an `unchanged` outcome means the + // user already had access (rare race) and is a silent no-op. + if (invitation.outcome === 'added') added.push(invitation.email) + } else { + successful.push(invitation.email) + } invitations.push(invitation) } catch (error) { if (error instanceof WorkspaceInvitationError) { @@ -102,6 +109,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { return NextResponse.json({ success: failed.length === 0, successful, + added, failed, invitations, }) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-invite-modal/organization-invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-invite-modal/organization-invite-modal.tsx index 121be256edc..50cd2d2d5b8 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-invite-modal/organization-invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-invite-modal/organization-invite-modal.tsx @@ -10,6 +10,7 @@ import { ChipModalField, ChipModalFooter, ChipModalHeader, + toast, } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import type { PermissionType } from '@/lib/workspaces/permissions/utils' @@ -112,7 +113,42 @@ export function OrganizationInviteModal({ inviteMember.mutate( { emails, orgId: organizationId, workspaceInvitations }, { - onSuccess: () => { + onSuccess: (result) => { + const summary = + 'data' in result && result.data && typeof result.data === 'object' + ? (result.data as { + invitationsSent?: number + directlyAddedCount?: number + failedInvitations?: Array<{ email: string; error: string }> + }) + : null + const addedCount = summary?.directlyAddedCount ?? 0 + const sentCount = summary?.invitationsSent ?? 0 + const failed = summary?.failedInvitations ?? [] + + // Surface partial successes even when some addresses fail. + const parts: string[] = [] + if (addedCount > 0) { + parts.push(`${addedCount} member${addedCount === 1 ? '' : 's'} added`) + } + if (sentCount > 0) { + parts.push(`${sentCount} invite${sentCount === 1 ? '' : 's'} sent`) + } + if (parts.length > 0) { + toast.success(parts.join(' · ')) + } + + if (failed.length > 0) { + // Keep only the failed addresses (workspaces stay selected) for retry. + setEmails(failed.map((entry) => entry.email)) + setErrorMessage( + failed.length === 1 + ? failed[0].error + : `${failed.length} invitations failed. ${failed[0].error}` + ) + return + } + setEmails([]) setSelectedWorkspaceIds([]) onOpenChange(false) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx index bb07a332c48..144fa39c69c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { ChipDropdown, ChipInput, @@ -12,6 +13,7 @@ import { DropdownMenuTrigger, MoreHorizontal, Search, + toast, } from '@/components/emcn' import type { OrgRole, PermissionType } from '@/components/permissions' import type { @@ -29,7 +31,10 @@ import { ManageCreditsModal, type ManageCreditsTarget, } from '@/app/workspace/[workspaceId]/settings/components/team-management/components/manage-credits-modal' -import { useUpdateWorkspacePermissions } from '@/hooks/queries/invitations' +import { + useRemoveWorkspaceMember, + useUpdateWorkspacePermissions, +} from '@/hooks/queries/invitations' import { useCancelInvitation, useResendInvitation, @@ -93,6 +98,7 @@ export function OrganizationMemberLists({ const updateMemberRole = useUpdateOrganizationMemberRole() const updateInvitation = useUpdateInvitation() const updatePermissions = useUpdateWorkspacePermissions() + const removeWorkspaceMember = useRemoveWorkspaceMember() const cancelInvitation = useCancelInvitation() const resendInvitation = useResendInvitation() @@ -300,8 +306,15 @@ export function OrganizationMemberLists({ access: RosterWorkspaceAccess ) => { const rowUserIsOrgAdmin = member.role === 'owner' || member.role === 'admin' - const wouldDemoteSelf = member.userId === currentUserId && access.permission === 'admin' + const isSelf = member.userId === currentUserId + const wouldDemoteSelf = isSelf && access.permission === 'admin' const disabled = rowUserIsOrgAdmin || wouldDemoteSelf || updatePermissions.isPending + /** + * Org owners/admins keep implicit admin access on org workspaces, so + * deleting their explicit permission row wouldn't actually revoke access. + * Only regular/external members can be removed from a single workspace. + */ + const canRemoveFromWorkspace = !rowUserIsOrgAdmin && !isSelf return ( } menu={buildActionsMenu( - copyToClipboard(member.email)}> - Copy email - + <> + copyToClipboard(member.email)}> + Copy email + + {canRemoveFromWorkspace && ( + + removeWorkspaceMember + .mutateAsync({ userId: member.userId, workspaceId, organizationId }) + .catch((error) => { + logger.error('Failed to remove workspace member', { error }) + toast.error("Couldn't remove member", { + description: getErrorMessage(error, 'Please try again in a moment.'), + }) + }) + } + > + Remove from workspace + + )} + )} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx index 600d7776a1f..5780f79532f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx @@ -9,6 +9,7 @@ import { ChipModalField, ChipModalFooter, ChipModalHeader, + toast, } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { isEnterprise } from '@/lib/billing/plan-helpers' @@ -100,11 +101,30 @@ export function InviteModal({ { workspaceId, organizationId, invitations }, { onSuccess: (result) => { + const parts: string[] = [] + if (result.added.length > 0) { + parts.push(`${result.added.length} member${result.added.length === 1 ? '' : 's'} added`) + } + if (result.successful.length > 0) { + parts.push( + `${result.successful.length} invite${result.successful.length === 1 ? '' : 's'} sent` + ) + } + if (parts.length > 0) { + toast.success(parts.join(' · ')) + } + if (result.failed.length > 0) { + // Keep the failed addresses in the field with the error for retry. setEmails(result.failed.map((f) => f.email)) - setErrorMessage(result.failed[0].error) + setErrorMessage( + result.failed.length === 1 + ? result.failed[0].error + : `${result.failed.length} invitations failed. ${result.failed[0].error}` + ) return } + setEmails([]) onOpenChange(false) }, diff --git a/apps/sim/components/emails/invitations/index.ts b/apps/sim/components/emails/invitations/index.ts index 6fa64cdc31c..383332020ac 100644 --- a/apps/sim/components/emails/invitations/index.ts +++ b/apps/sim/components/emails/invitations/index.ts @@ -1,4 +1,5 @@ export { BatchInvitationEmail } from './batch-invitation-email' export { InvitationEmail } from './invitation-email' export { PollingGroupInvitationEmail } from './polling-group-invitation-email' +export { WorkspaceAddedEmail } from './workspace-added-email' export { WorkspaceInvitationEmail } from './workspace-invitation-email' diff --git a/apps/sim/components/emails/invitations/workspace-added-email.tsx b/apps/sim/components/emails/invitations/workspace-added-email.tsx new file mode 100644 index 00000000000..3e291f48048 --- /dev/null +++ b/apps/sim/components/emails/invitations/workspace-added-email.tsx @@ -0,0 +1,44 @@ +import { Link, Text } from '@react-email/components' +import { baseStyles } from '@/components/emails/_styles' +import { EmailLayout } from '@/components/emails/components' +import { getBrandConfig } from '@/ee/whitelabeling' + +interface WorkspaceAddedEmailProps { + /** Name of the workspace the recipient was added to. */ + workspaceName?: string + /** Name of the person who added the recipient. */ + inviterName?: string + /** Direct link to the workspace (no acceptance required). */ + workspaceLink?: string +} + +export function WorkspaceAddedEmail({ + workspaceName = 'Workspace', + inviterName = 'Someone', + workspaceLink = '', +}: WorkspaceAddedEmailProps) { + const brand = getBrandConfig() + const preview = `You've been added to the "${workspaceName}" workspace on ${brand.name}` + + return ( + + Hello, + + {inviterName} added you to the {workspaceName} workspace + on {brand.name}. + + + + Open workspace + + +
+ + + If this was unexpected, contact a workspace admin. + + + ) +} + +export default WorkspaceAddedEmail diff --git a/apps/sim/components/emails/render.ts b/apps/sim/components/emails/render.ts index bb601bcbcd9..92318b61069 100644 --- a/apps/sim/components/emails/render.ts +++ b/apps/sim/components/emails/render.ts @@ -20,6 +20,7 @@ import { BatchInvitationEmail, InvitationEmail, PollingGroupInvitationEmail, + WorkspaceAddedEmail, WorkspaceInvitationEmail, } from '@/components/emails/invitations' import { HelpConfirmationEmail } from '@/components/emails/support' @@ -215,6 +216,20 @@ export async function renderWorkspaceInvitationEmail( ) } +export async function renderWorkspaceAddedEmail( + inviterName: string, + workspaceName: string, + workspaceLink: string +): Promise { + return await render( + WorkspaceAddedEmail({ + inviterName, + workspaceName, + workspaceLink, + }) + ) +} + export async function renderPollingGroupInvitationEmail(params: { inviterName: string organizationName: string diff --git a/apps/sim/components/emails/subjects.ts b/apps/sim/components/emails/subjects.ts index a1ddbd3ed3b..e9630289500 100644 --- a/apps/sim/components/emails/subjects.ts +++ b/apps/sim/components/emails/subjects.ts @@ -10,6 +10,7 @@ export type EmailSubjectType = | 'existing-account' | 'invitation' | 'batch-invitation' + | 'workspace-added' | 'polling-group-invitation' | 'help-confirmation' | 'enterprise-subscription' @@ -48,6 +49,8 @@ export function getEmailSubject(type: EmailSubjectType): string { return `You've been invited to join a team on ${brandName}` case 'batch-invitation': return `You've been invited to join a team and workspaces on ${brandName}` + case 'workspace-added': + return `You've been added to a workspace on ${brandName}` case 'polling-group-invitation': return `You've been invited to join an email polling group on ${brandName}` case 'help-confirmation': diff --git a/apps/sim/hooks/queries/invitations.ts b/apps/sim/hooks/queries/invitations.ts index d9b35a434d1..17e0bfe3963 100644 --- a/apps/sim/hooks/queries/invitations.ts +++ b/apps/sim/hooks/queries/invitations.ts @@ -70,11 +70,15 @@ type BatchSendInvitationsParams = ContractBodyInput +type BatchInvitationResult = Pick & { + added: string[] +} /** * Sends workspace invitations through the server-side batch endpoint. - * Returns results for each invitation indicating success or failure. + * Returns results for each invitation indicating success or failure. Existing + * organization members are added directly (no acceptance) and reported in + * `added`; everyone else receives a pending invitation in `successful`. */ export function useBatchSendWorkspaceInvitations() { const queryClient = useQueryClient() @@ -93,6 +97,7 @@ export function useBatchSendWorkspaceInvitations() { return { successful: result.successful ?? [], + added: result.added ?? [], failed: result.failed ?? [], } }, @@ -100,6 +105,12 @@ export function useBatchSendWorkspaceInvitations() { queryClient.invalidateQueries({ queryKey: invitationKeys.list(variables.workspaceId), }) + queryClient.invalidateQueries({ + queryKey: workspaceKeys.permissions(variables.workspaceId), + }) + queryClient.invalidateQueries({ + queryKey: workspaceKeys.members(variables.workspaceId), + }) if (variables.organizationId) { queryClient.invalidateQueries({ queryKey: organizationKeys.roster(variables.organizationId), diff --git a/apps/sim/hooks/queries/organization.ts b/apps/sim/hooks/queries/organization.ts index ac02eeacccb..1b38e66e42c 100644 --- a/apps/sim/hooks/queries/organization.ts +++ b/apps/sim/hooks/queries/organization.ts @@ -414,7 +414,14 @@ export function useInviteMember() { return useMutation({ mutationFn: async ({ emails, workspaceInvitations, orgId }: InviteMemberParams) => { - const result = await requestJson(inviteOrganizationMembersContract, { + /** + * Partial batches return HTTP 207 with `success: false` and a `data` + * payload (some invited/added, some failed). `requestJson` only throws on + * >= 400 (e.g. the total-failure 502 / validation 400 paths), so partials + * resolve here and the caller reports successes + per-email failures from + * `data` instead of surfacing a single generic error. + */ + return requestJson(inviteOrganizationMembersContract, { params: { id: orgId }, query: { batch: true }, body: { @@ -422,12 +429,6 @@ export function useInviteMember() { workspaceInvitations, }, }) - - if (result.success === false) { - throw new Error(result.error || result.message || 'Failed to invite teammate') - } - - return result }, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ queryKey: organizationKeys.detail(variables.orgId) }) @@ -435,6 +436,15 @@ export function useInviteMember() { queryClient.invalidateQueries({ queryKey: organizationKeys.memberUsage(variables.orgId) }) queryClient.invalidateQueries({ queryKey: organizationKeys.roster(variables.orgId) }) queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }) + // Existing members may have been added directly to selected workspaces. + for (const grant of variables.workspaceInvitations ?? []) { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.permissions(grant.workspaceId), + }) + queryClient.invalidateQueries({ + queryKey: workspaceKeys.members(grant.workspaceId), + }) + } }, }) } diff --git a/apps/sim/lib/api/contracts/invitations.ts b/apps/sim/lib/api/contracts/invitations.ts index 5631bdc7418..b1fa8871523 100644 --- a/apps/sim/lib/api/contracts/invitations.ts +++ b/apps/sim/lib/api/contracts/invitations.ts @@ -53,6 +53,7 @@ export const batchInvitationResultSchema = z .object({ success: z.boolean(), successful: z.array(z.string()), + added: z.array(z.string()).optional(), failed: z.array(z.object({ email: z.string(), error: z.string() })), invitations: z.array(z.record(z.string(), z.unknown())), }) diff --git a/apps/sim/lib/api/contracts/organization.ts b/apps/sim/lib/api/contracts/organization.ts index 28606fcc722..eaa4c3dbf75 100644 --- a/apps/sim/lib/api/contracts/organization.ts +++ b/apps/sim/lib/api/contracts/organization.ts @@ -310,6 +310,8 @@ export const inviteOrganizationMembersContract = defineRouteContract({ .object({ invitationsSent: z.number(), invitedEmails: z.array(z.string()), + directlyAdded: z.array(z.string()).optional(), + directlyAddedCount: z.number().optional(), failedInvitations: z.array(z.object({ email: z.string(), error: z.string() })), existingMembers: z.array(z.string()), pendingInvitations: z.array(z.string()), diff --git a/apps/sim/lib/core/telemetry.ts b/apps/sim/lib/core/telemetry.ts index a77ab240990..1b67c865edb 100644 --- a/apps/sim/lib/core/telemetry.ts +++ b/apps/sim/lib/core/telemetry.ts @@ -554,6 +554,24 @@ export const PlatformEvents = { }) }, + /** + * Track member added directly to a workspace (no acceptance step) because + * they were already a member of the workspace's organization. + */ + workspaceMemberAdded: (attrs: { + workspaceId: string + addedBy: string + addedUserId: string + role: string + }) => { + trackPlatformEvent('platform.workspace.member_added', { + 'workspace.id': attrs.workspaceId, + 'user.id': attrs.addedBy, + 'member.id': attrs.addedUserId, + 'member.role': attrs.role, + }) + }, + /** * Track member joined workspace */ diff --git a/apps/sim/lib/invitations/core.ts b/apps/sim/lib/invitations/core.ts index fb2fa173886..c8152bad844 100644 --- a/apps/sim/lib/invitations/core.ts +++ b/apps/sim/lib/invitations/core.ts @@ -33,8 +33,8 @@ import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' const logger = createLogger('InvitationCore') -const PERMISSION_RANK = { read: 0, write: 1, admin: 2 } as const -type PermissionLevel = keyof typeof PERMISSION_RANK +export const PERMISSION_RANK = { read: 0, write: 1, admin: 2 } as const +export type PermissionLevel = keyof typeof PERMISSION_RANK export const INVITATION_EXPIRY_DAYS = 7 diff --git a/apps/sim/lib/invitations/direct-grant.test.ts b/apps/sim/lib/invitations/direct-grant.test.ts new file mode 100644 index 00000000000..9eba697ffff --- /dev/null +++ b/apps/sim/lib/invitations/direct-grant.test.ts @@ -0,0 +1,214 @@ +/** + * @vitest-environment node + */ +import { + auditMock, + auditMockFns, + dbChainMock, + dbChainMockFns, + resetDbChainMock, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetUserOrganization, + mockSyncWorkspaceEnvCredentials, + mockCancelPendingInvitation, + mockSendWorkspaceAddedEmail, + mockCaptureServerEvent, + mockWorkspaceMemberAdded, +} = vi.hoisted(() => ({ + mockGetUserOrganization: vi.fn(), + mockSyncWorkspaceEnvCredentials: vi.fn(), + mockCancelPendingInvitation: vi.fn(), + mockSendWorkspaceAddedEmail: vi.fn(), + mockCaptureServerEvent: vi.fn(), + mockWorkspaceMemberAdded: vi.fn(), +})) + +vi.mock('@sim/db', () => dbChainMock) +vi.mock('@sim/audit', () => auditMock) + +vi.mock('@/lib/billing/organizations/membership', () => ({ + getUserOrganization: mockGetUserOrganization, +})) + +vi.mock('@/lib/core/telemetry', () => ({ + PlatformEvents: { workspaceMemberAdded: mockWorkspaceMemberAdded }, +})) + +vi.mock('@/lib/credentials/environment', () => ({ + syncWorkspaceEnvCredentials: mockSyncWorkspaceEnvCredentials, +})) + +vi.mock('@/lib/invitations/send', () => ({ + cancelPendingInvitation: mockCancelPendingInvitation, + sendWorkspaceAddedEmail: mockSendWorkspaceAddedEmail, +})) + +vi.mock('@/lib/posthog/server', () => ({ + captureServerEvent: mockCaptureServerEvent, +})) + +import { grantWorkspaceAccessDirectly, isSameOrgMember } from '@/lib/invitations/direct-grant' + +/** + * Drives `db.select().from().where()` results in call order. Both an awaited + * `where()` and a chained `.limit()` resolve to the same per-call value. + */ +function queueWhereResponses(responses: unknown[][]) { + const queue = [...responses] + dbChainMockFns.where.mockImplementation(() => { + const result = queue.shift() ?? [] + const thenable = Promise.resolve(result) as Promise & { + limit: ReturnType + orderBy: ReturnType + returning: ReturnType + groupBy: ReturnType + } + thenable.limit = vi.fn(() => Promise.resolve(result)) + thenable.orderBy = vi.fn(() => Promise.resolve(result)) + thenable.returning = vi.fn(() => Promise.resolve(result)) + thenable.groupBy = vi.fn(() => Promise.resolve(result)) + return thenable as ReturnType + }) +} + +const baseInput = { + userId: 'user-2', + email: 'Member@Example.com', + workspaceId: 'ws-1', + workspaceName: 'Workspace 1', + permission: 'write' as const, + organizationId: 'org-1', + actorId: 'user-1', + actorName: 'Owner', + actorEmail: 'owner@example.com', +} + +describe('grantWorkspaceAccessDirectly', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockSendWorkspaceAddedEmail.mockResolvedValue({ success: true }) + // Insert path reports the new row via `.returning()`. + dbChainMockFns.returning.mockResolvedValue([{ id: 'perm-new' }]) + }) + + it('inserts a permission row when the user has no existing access', async () => { + const result = await grantWorkspaceAccessDirectly({ ...baseInput }) + + expect(result).toEqual({ outcome: 'added', permission: 'write' }) + expect(dbChainMockFns.insert).toHaveBeenCalled() + expect(dbChainMockFns.update).not.toHaveBeenCalled() + expect(auditMockFns.mockRecordAudit).toHaveBeenCalledWith( + expect.objectContaining({ action: 'member.added', resourceId: 'ws-1' }) + ) + expect(mockWorkspaceMemberAdded).toHaveBeenCalledWith( + expect.objectContaining({ workspaceId: 'ws-1' }) + ) + expect(mockSendWorkspaceAddedEmail).toHaveBeenCalledWith( + expect.objectContaining({ email: 'member@example.com', workspaceId: 'ws-1' }) + ) + }) + + it('reports unchanged (no audit/email) when a concurrent insert wins the race', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([]) + + const result = await grantWorkspaceAccessDirectly({ ...baseInput }) + + expect(result).toEqual({ outcome: 'unchanged', permission: 'write' }) + expect(dbChainMockFns.insert).toHaveBeenCalled() + expect(auditMockFns.mockRecordAudit).not.toHaveBeenCalled() + expect(mockWorkspaceMemberAdded).not.toHaveBeenCalled() + expect(mockSendWorkspaceAddedEmail).not.toHaveBeenCalled() + }) + + it('does not upgrade an existing lower permission (invites never modify access)', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'perm-1', permissionType: 'read' }]) + + const result = await grantWorkspaceAccessDirectly({ ...baseInput, permission: 'admin' }) + + expect(result).toEqual({ outcome: 'unchanged', permission: 'read' }) + expect(dbChainMockFns.update).not.toHaveBeenCalled() + expect(dbChainMockFns.insert).not.toHaveBeenCalled() + expect(auditMockFns.mockRecordAudit).not.toHaveBeenCalled() + expect(mockSendWorkspaceAddedEmail).not.toHaveBeenCalled() + }) + + it('no-ops when the user already has access', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'perm-1', permissionType: 'admin' }]) + + const result = await grantWorkspaceAccessDirectly({ ...baseInput, permission: 'write' }) + + expect(result).toEqual({ outcome: 'unchanged', permission: 'admin' }) + expect(dbChainMockFns.insert).not.toHaveBeenCalled() + expect(dbChainMockFns.update).not.toHaveBeenCalled() + expect(auditMockFns.mockRecordAudit).not.toHaveBeenCalled() + expect(mockWorkspaceMemberAdded).not.toHaveBeenCalled() + expect(mockSendWorkspaceAddedEmail).not.toHaveBeenCalled() + }) + + it('skips the email when notify is false', async () => { + const result = await grantWorkspaceAccessDirectly({ ...baseInput, notify: false }) + + expect(result.outcome).toBe('added') + expect(auditMockFns.mockRecordAudit).toHaveBeenCalled() + expect(mockSendWorkspaceAddedEmail).not.toHaveBeenCalled() + }) + + it('syncs workspace env credentials when env variables exist', async () => { + dbChainMockFns.limit + .mockResolvedValueOnce([]) // existing permission lookup + .mockResolvedValueOnce([{ variables: { API_KEY: 'x', BASE_URL: 'y' } }]) // env lookup + + await grantWorkspaceAccessDirectly({ ...baseInput }) + + expect(mockSyncWorkspaceEnvCredentials).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceId: 'ws-1', + actingUserId: 'user-2', + envKeys: ['API_KEY', 'BASE_URL'], + }) + ) + }) + + it('supersedes lingering pending workspace invitations for the same email', async () => { + queueWhereResponses([ + [], // existing permission lookup (transaction) + [{ invitationId: 'old-inv' }], // supersede lookup + [], // env lookup + ]) + + await grantWorkspaceAccessDirectly({ ...baseInput }) + + expect(mockCancelPendingInvitation).toHaveBeenCalledWith('old-inv') + }) +}) + +describe('isSameOrgMember', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('returns false when the workspace has no organization', async () => { + expect(await isSameOrgMember('user-2', null)).toBe(false) + expect(mockGetUserOrganization).not.toHaveBeenCalled() + }) + + it('returns false when the user belongs to no organization', async () => { + mockGetUserOrganization.mockResolvedValueOnce(null) + expect(await isSameOrgMember('user-2', 'org-1')).toBe(false) + }) + + it('returns true when the user belongs to the workspace organization', async () => { + mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'member' }) + expect(await isSameOrgMember('user-2', 'org-1')).toBe(true) + }) + + it('returns false when the user belongs to a different organization', async () => { + mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-2', role: 'member' }) + expect(await isSameOrgMember('user-2', 'org-1')).toBe(false) + }) +}) diff --git a/apps/sim/lib/invitations/direct-grant.ts b/apps/sim/lib/invitations/direct-grant.ts new file mode 100644 index 00000000000..93118f7034a --- /dev/null +++ b/apps/sim/lib/invitations/direct-grant.ts @@ -0,0 +1,235 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { db } from '@sim/db' +import { + invitation, + invitationWorkspaceGrant, + permissions, + workspaceEnvironment, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { normalizeEmail } from '@sim/utils/string' +import { and, eq } from 'drizzle-orm' +import type { NextRequest } from 'next/server' +import { getUserOrganization } from '@/lib/billing/organizations/membership' +import { PlatformEvents } from '@/lib/core/telemetry' +import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { cancelPendingInvitation, sendWorkspaceAddedEmail } from '@/lib/invitations/send' +import { captureServerEvent } from '@/lib/posthog/server' +import type { PermissionType } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('InvitationDirectGrant') + +export type DirectGrantOutcome = + | { outcome: 'added'; permission: PermissionType } + | { outcome: 'unchanged'; permission: PermissionType } + +export interface GrantWorkspaceAccessDirectlyInput { + /** Registered user receiving access. */ + userId: string + /** Invitee email (used for notification + audit; normalized internally). */ + email: string + workspaceId: string + workspaceName: string + permission: PermissionType + /** Organization that owns the workspace. */ + organizationId: string + actorId: string + actorName: string + actorEmail?: string | null + request?: NextRequest + /** Send the lightweight "you've been added" email. Defaults to true. */ + notify?: boolean +} + +/** + * Returns whether the given user is already a member of the workspace's + * organization. Only same-org members are eligible for direct (no-acceptance) + * workspace access. + */ +export async function isSameOrgMember( + userId: string, + workspaceOrganizationId: string | null +): Promise { + if (!workspaceOrganizationId) return false + const membership = await getUserOrganization(userId) + return !!membership && membership.organizationId === workspaceOrganizationId +} + +/** + * Cancels any pending single-workspace invitations that grant exactly this + * workspace to this email. Multi-workspace organization invitations are left + * untouched — their remaining grants stay valid and the accept flow upserts + * permissions idempotently. + */ +async function supersedePendingWorkspaceInvites( + workspaceId: string, + normalizedEmail: string +): Promise { + const rows = await db + .select({ invitationId: invitation.id }) + .from(invitation) + .innerJoin(invitationWorkspaceGrant, eq(invitationWorkspaceGrant.invitationId, invitation.id)) + .where( + and( + eq(invitation.kind, 'workspace'), + eq(invitation.email, normalizedEmail), + eq(invitation.status, 'pending'), + eq(invitationWorkspaceGrant.workspaceId, workspaceId) + ) + ) + + for (const row of rows) { + await cancelPendingInvitation(row.invitationId) + } +} + +/** + * Grants a user workspace access immediately, without an invitation or + * acceptance step. Intended for users who already belong to the workspace's + * organization and are not yet members of the workspace. Idempotent: when a + * permission already exists it is left untouched (no-op) — invites never modify + * or upgrade an existing member's permission. + */ +export async function grantWorkspaceAccessDirectly( + input: GrantWorkspaceAccessDirectlyInput +): Promise { + const normalizedEmail = normalizeEmail(input.email) + + const result = await db.transaction(async (tx): Promise => { + const [existing] = await tx + .select({ id: permissions.id, permissionType: permissions.permissionType }) + .from(permissions) + .where( + and( + eq(permissions.entityId, input.workspaceId), + eq(permissions.entityType, 'workspace'), + eq(permissions.userId, input.userId) + ) + ) + .limit(1) + + if (existing) { + return { outcome: 'unchanged', permission: existing.permissionType as PermissionType } + } + + const inserted = await tx + .insert(permissions) + .values({ + id: generateId(), + entityType: 'workspace', + entityId: input.workspaceId, + userId: input.userId, + permissionType: input.permission, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing() + .returning({ id: permissions.id }) + + if (inserted.length === 0) { + return { outcome: 'unchanged', permission: input.permission } + } + + return { outcome: 'added', permission: input.permission } + }) + + if (result.outcome === 'unchanged') { + return result + } + + try { + await supersedePendingWorkspaceInvites(input.workspaceId, normalizedEmail) + } catch (error) { + logger.error('Failed to supersede pending workspace invitations after direct grant', { + workspaceId: input.workspaceId, + error, + }) + } + + try { + const [wsEnvRow] = await db + .select({ variables: workspaceEnvironment.variables }) + .from(workspaceEnvironment) + .where(eq(workspaceEnvironment.workspaceId, input.workspaceId)) + .limit(1) + const wsEnvKeys = Object.keys((wsEnvRow?.variables as Record) || {}) + if (wsEnvKeys.length > 0) { + await syncWorkspaceEnvCredentials({ + workspaceId: input.workspaceId, + envKeys: wsEnvKeys, + actingUserId: input.userId, + }) + } + } catch (error) { + logger.error('Failed to sync workspace env credentials after direct grant', { + workspaceId: input.workspaceId, + userId: input.userId, + error, + }) + } + + try { + PlatformEvents.workspaceMemberAdded({ + workspaceId: input.workspaceId, + addedBy: input.actorId, + addedUserId: input.userId, + role: input.permission, + }) + } catch { + /** + * Telemetry must not fail the grant. + */ + } + + captureServerEvent( + input.actorId, + 'workspace_member_added', + { + workspace_id: input.workspaceId, + member_role: input.permission, + }, + { + groups: { workspace: input.workspaceId }, + } + ) + + recordAudit({ + workspaceId: input.workspaceId, + actorId: input.actorId, + actorName: input.actorName, + actorEmail: input.actorEmail, + action: AuditAction.MEMBER_ADDED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: input.workspaceId, + resourceName: normalizedEmail, + description: `Added existing organization member ${normalizedEmail} as ${input.permission}`, + metadata: { + targetEmail: normalizedEmail, + targetRole: input.permission, + organizationId: input.organizationId, + workspaceName: input.workspaceName, + addedUserId: input.userId, + }, + request: input.request, + }) + + if (input.notify ?? true) { + try { + await sendWorkspaceAddedEmail({ + email: normalizedEmail, + inviterName: input.actorName, + workspaceId: input.workspaceId, + workspaceName: input.workspaceName, + }) + } catch (error) { + logger.error('Failed to send workspace added email', { + workspaceId: input.workspaceId, + email: normalizedEmail, + error, + }) + } + } + + return result +} diff --git a/apps/sim/lib/invitations/send.ts b/apps/sim/lib/invitations/send.ts index 23b7f8566c6..6dc32b540a6 100644 --- a/apps/sim/lib/invitations/send.ts +++ b/apps/sim/lib/invitations/send.ts @@ -15,12 +15,14 @@ import { getEmailSubject, renderBatchInvitationEmail, renderInvitationEmail, + renderWorkspaceAddedEmail, renderWorkspaceInvitationEmail, } from '@/components/emails' import { getBaseUrl } from '@/lib/core/utils/urls' import { computeInvitationExpiry } from '@/lib/invitations/core' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress } from '@/lib/messaging/email/utils' +import { getBrandConfig } from '@/ee/whitelabeling' const logger = createLogger('InvitationSend') @@ -205,10 +207,11 @@ export async function sendInvitationEmail( inviteUrl ) + const brandName = getBrandConfig().name const subject = workspaceNames.length === 1 - ? `You've been invited to join "${workspaceNames[0]}" on Sim` - : `You've been invited to join ${workspaceNames.length} workspaces on Sim` + ? `You've been invited to join "${workspaceNames[0]}" on ${brandName}` + : `You've been invited to join ${workspaceNames.length} workspaces on ${brandName}` const result = await sendEmail({ to: input.email, @@ -281,6 +284,41 @@ export async function sendInvitationEmail( return { success: true } } +export interface SendWorkspaceAddedEmailInput { + email: string + inviterName: string + workspaceId: string + workspaceName: string +} + +/** + * Lightweight notification sent when an existing organization member is added + * directly to a workspace. Unlike an invitation email, this links straight to + * the workspace and has no acceptance step. + */ +export async function sendWorkspaceAddedEmail( + input: SendWorkspaceAddedEmailInput +): Promise { + const workspaceLink = `${getBaseUrl()}/workspace/${input.workspaceId}/home` + const emailHtml = await renderWorkspaceAddedEmail( + input.inviterName, + input.workspaceName, + workspaceLink + ) + + const result = await sendEmail({ + to: input.email, + subject: getEmailSubject('workspace-added'), + html: emailHtml, + from: getFromEmailAddress(), + emailType: 'transactional', + }) + if (!result.success) { + return { success: false, error: result.message } + } + return { success: true } +} + export async function prepareInvitationResend(params: { invitationId: string rotateToken?: boolean diff --git a/apps/sim/lib/invitations/workspace-invitations.test.ts b/apps/sim/lib/invitations/workspace-invitations.test.ts new file mode 100644 index 00000000000..a63fc27447a --- /dev/null +++ b/apps/sim/lib/invitations/workspace-invitations.test.ts @@ -0,0 +1,231 @@ +/** + * @vitest-environment node + */ +import { + auditMock, + createMockRequest, + dbChainMock, + dbChainMockFns, + resetDbChainMock, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetUserOrganization, + mockValidateSeatAvailability, + mockGrantWorkspaceAccessDirectly, + mockCreatePendingInvitation, + mockSendInvitationEmail, + mockCancelPendingInvitation, + mockFindPendingGrantForWorkspaceEmail, + mockWorkspaceMemberInvited, + mockCaptureServerEvent, +} = vi.hoisted(() => ({ + mockGetUserOrganization: vi.fn(), + mockValidateSeatAvailability: vi.fn(), + mockGrantWorkspaceAccessDirectly: vi.fn(), + mockCreatePendingInvitation: vi.fn(), + mockSendInvitationEmail: vi.fn(), + mockCancelPendingInvitation: vi.fn(), + mockFindPendingGrantForWorkspaceEmail: vi.fn(), + mockWorkspaceMemberInvited: vi.fn(), + mockCaptureServerEvent: vi.fn(), +})) + +vi.mock('@sim/db', () => dbChainMock) +vi.mock('@sim/audit', () => auditMock) + +vi.mock('@/lib/billing/organizations/membership', () => ({ + getUserOrganization: mockGetUserOrganization, +})) + +vi.mock('@/lib/billing/validation/seat-management', () => ({ + validateSeatAvailability: mockValidateSeatAvailability, +})) + +vi.mock('@/lib/core/telemetry', () => ({ + PlatformEvents: { workspaceMemberInvited: mockWorkspaceMemberInvited }, +})) + +vi.mock('@/lib/invitations/direct-grant', () => ({ + grantWorkspaceAccessDirectly: mockGrantWorkspaceAccessDirectly, +})) + +vi.mock('@/lib/invitations/send', () => ({ + createPendingInvitation: mockCreatePendingInvitation, + sendInvitationEmail: mockSendInvitationEmail, + cancelPendingInvitation: mockCancelPendingInvitation, + findPendingGrantForWorkspaceEmail: mockFindPendingGrantForWorkspaceEmail, +})) + +vi.mock('@/lib/posthog/server', () => ({ + captureServerEvent: mockCaptureServerEvent, +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + getWorkspaceWithOwner: vi.fn(), +})) + +vi.mock('@/lib/workspaces/policy', () => ({ + getWorkspaceInvitePolicy: vi.fn(), +})) + +vi.mock('@/ee/access-control/utils/permission-check', () => ({ + validateInvitationsAllowed: vi.fn(), +})) + +import { createWorkspaceInvitation } from '@/lib/invitations/workspace-invitations' + +function queueWhereResponses(responses: unknown[][]) { + const queue = [...responses] + dbChainMockFns.where.mockImplementation(() => { + const result = queue.shift() ?? [] + const thenable = Promise.resolve(result) as Promise & { + limit: ReturnType + } + thenable.limit = vi.fn(() => Promise.resolve(result)) + return thenable as ReturnType + }) +} + +function makeContext() { + return { + workspaceId: 'ws-1', + inviterId: 'user-1', + inviterName: 'Owner', + inviterEmail: 'owner@example.com', + workspaceDetails: { + id: 'ws-1', + name: 'Workspace 1', + ownerId: 'user-1', + organizationId: 'org-1', + billedAccountUserId: 'user-1', + }, + invitePolicy: { + allowed: true, + reason: null, + requiresSeat: false, + organizationId: 'org-1', + upgradeRequired: false, + }, + // The function only reads the fields above at runtime. + } as Parameters[0]['context'] +} + +const request = createMockRequest( + 'POST', + {}, + {}, + 'http://localhost/api/workspaces/invitations/batch' +) + +describe('createWorkspaceInvitation', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockGrantWorkspaceAccessDirectly.mockResolvedValue({ outcome: 'added', permission: 'write' }) + mockCreatePendingInvitation.mockResolvedValue({ invitationId: 'inv-1', token: 'tok-1' }) + mockSendInvitationEmail.mockResolvedValue({ success: true }) + mockFindPendingGrantForWorkspaceEmail.mockResolvedValue(null) + }) + + it('directly grants access to an existing member of the workspace organization', async () => { + queueWhereResponses([[{ id: 'user-2', email: 'member@example.com' }], []]) + mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'member' }) + + const result = await createWorkspaceInvitation({ + context: makeContext(), + email: 'member@example.com', + permission: 'write', + request, + }) + + expect(result.instantAdd).toBe(true) + expect(result.outcome).toBe('added') + expect(mockGrantWorkspaceAccessDirectly).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 'user-2', + workspaceId: 'ws-1', + permission: 'write', + organizationId: 'org-1', + }) + ) + expect(mockCreatePendingInvitation).not.toHaveBeenCalled() + expect(mockSendInvitationEmail).not.toHaveBeenCalled() + }) + + it('rejects an existing workspace member without upgrading their permission', async () => { + queueWhereResponses([ + [{ id: 'user-2', email: 'member@example.com' }], + [{ id: 'perm-1', permissionType: 'read' }], + ]) + mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'member' }) + + await expect( + createWorkspaceInvitation({ + context: makeContext(), + email: 'member@example.com', + permission: 'admin', + request, + }) + ).rejects.toThrow('already has access') + + expect(mockGrantWorkspaceAccessDirectly).not.toHaveBeenCalled() + expect(mockCreatePendingInvitation).not.toHaveBeenCalled() + }) + + it('creates an external pending invitation when the user belongs to a different org', async () => { + queueWhereResponses([[{ id: 'user-3', email: 'ext@example.com' }], []]) + mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-2', role: 'member' }) + + const result = await createWorkspaceInvitation({ + context: makeContext(), + email: 'ext@example.com', + permission: 'read', + request, + }) + + expect(result.instantAdd).toBeFalsy() + expect(mockGrantWorkspaceAccessDirectly).not.toHaveBeenCalled() + expect(mockCreatePendingInvitation).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'workspace', membershipIntent: 'external' }) + ) + expect(mockSendInvitationEmail).toHaveBeenCalled() + }) + + it('creates an internal pending invitation when the registered user has no org', async () => { + queueWhereResponses([[{ id: 'user-4', email: 'noorg@example.com' }], []]) + mockGetUserOrganization.mockResolvedValueOnce(null) + + const result = await createWorkspaceInvitation({ + context: makeContext(), + email: 'noorg@example.com', + permission: 'write', + request, + }) + + expect(result.instantAdd).toBeFalsy() + expect(mockGrantWorkspaceAccessDirectly).not.toHaveBeenCalled() + expect(mockCreatePendingInvitation).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'workspace', membershipIntent: 'internal' }) + ) + }) + + it('creates a pending invitation for a brand-new email', async () => { + queueWhereResponses([[]]) + + const result = await createWorkspaceInvitation({ + context: makeContext(), + email: 'new@example.com', + permission: 'read', + request, + }) + + expect(result.instantAdd).toBeFalsy() + expect(mockGetUserOrganization).not.toHaveBeenCalled() + expect(mockGrantWorkspaceAccessDirectly).not.toHaveBeenCalled() + expect(mockCreatePendingInvitation).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'workspace', membershipIntent: 'internal' }) + ) + }) +}) diff --git a/apps/sim/lib/invitations/workspace-invitations.ts b/apps/sim/lib/invitations/workspace-invitations.ts index c29cbc12731..63de5039649 100644 --- a/apps/sim/lib/invitations/workspace-invitations.ts +++ b/apps/sim/lib/invitations/workspace-invitations.ts @@ -7,6 +7,10 @@ import type { NextRequest } from 'next/server' import { getUserOrganization } from '@/lib/billing/organizations/membership' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' import { PlatformEvents } from '@/lib/core/telemetry' +import { + type DirectGrantOutcome, + grantWorkspaceAccessDirectly, +} from '@/lib/invitations/direct-grant' import { cancelPendingInvitation, createPendingInvitation, @@ -38,6 +42,10 @@ export interface WorkspaceInvitationResult { permission: PermissionType membershipIntent: InvitationMembershipIntent expiresAt: Date | undefined + /** True when the user was granted access directly (no pending invitation). */ + instantAdd?: boolean + /** Direct-grant outcome when `instantAdd` is true. */ + outcome?: DirectGrantOutcome['outcome'] } export class WorkspaceInvitationError extends Error { @@ -152,6 +160,11 @@ export async function createWorkspaceInvitation({ .then((rows) => rows[0]) if (existingUser) { + const workspaceOrganizationId = context.workspaceDetails.organizationId + const existingMembership = workspaceOrganizationId + ? await getUserOrganization(existingUser.id) + : null + const existingPermission = await db .select() .from(permissions) @@ -164,6 +177,11 @@ export async function createWorkspaceInvitation({ ) .then((rows) => rows[0]) + /** + * Already a workspace member: reject. Invites never change an existing + * member's permission — role changes go through the members list, not the + * invite flow. (The client also blocks re-inviting current teammates.) + */ if (existingPermission) { throw new WorkspaceInvitationError({ message: `${normalizedEmail} already has access to this workspace`, @@ -172,18 +190,46 @@ export async function createWorkspaceInvitation({ }) } - if (context.invitePolicy.organizationId) { - const existingMembership = await getUserOrganization(existingUser.id) - if ( - existingMembership && - existingMembership.organizationId !== context.invitePolicy.organizationId - ) { + /** + * Invitee already belongs to the workspace's organization (and is not yet a + * member of this workspace): grant access directly, with no invitation or + * acceptance step. + */ + if ( + workspaceOrganizationId && + existingMembership && + existingMembership.organizationId === workspaceOrganizationId + ) { + const directGrant = await grantWorkspaceAccessDirectly({ + userId: existingUser.id, + email: normalizedEmail, + workspaceId: context.workspaceId, + workspaceName: context.workspaceDetails.name, + permission: invitationPermission, + organizationId: workspaceOrganizationId, + actorId: context.inviterId, + actorName: context.inviterName, + actorEmail: context.inviterEmail, + request, + }) + + return { + id: existingUser.id, + workspaceId: context.workspaceId, + email: normalizedEmail, + permission: invitationPermission, + membershipIntent: 'internal', + expiresAt: undefined, + instantAdd: true, + outcome: directGrant.outcome, + } + } + + if (workspaceOrganizationId) { + if (existingMembership && existingMembership.organizationId !== workspaceOrganizationId) { membershipIntent = 'external' } else if (context.invitePolicy.requiresSeat && !existingMembership) { - const seatValidation = await validateSeatAvailability( - context.invitePolicy.organizationId, - 1 - ) + const seatValidation = await validateSeatAvailability(workspaceOrganizationId, 1) if (!seatValidation.canInvite) { throw new WorkspaceInvitationError({ message: seatValidation.reason || 'No available seats for this organization.', diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 02dae0d6b02..ed17ba7e2a8 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -91,6 +91,11 @@ export interface PostHogEventMap { membership_intent?: string } + workspace_member_added: { + workspace_id: string + member_role: string + } + workspace_member_removed: { workspace_id: string is_self_removal: boolean diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 2757f2988f0..7eccc1f757f 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -99,6 +99,7 @@ export const AuditAction = { // Members MEMBER_INVITED: 'member.invited', + MEMBER_ADDED: 'member.added', MEMBER_REMOVED: 'member.removed', MEMBER_ROLE_CHANGED: 'member.role_changed', diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 30c2a71bb2d..83e6d90d710 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -99,6 +99,7 @@ export const auditMock = { MCP_SERVER_UPDATED: 'mcp_server.updated', MCP_SERVER_REMOVED: 'mcp_server.removed', MEMBER_INVITED: 'member.invited', + MEMBER_ADDED: 'member.added', MEMBER_REMOVED: 'member.removed', MEMBER_ROLE_CHANGED: 'member.role_changed', OAUTH_DISCONNECTED: 'oauth.disconnected', From f0b3550729ce4dcd8aef8dc8bf75762960cb1c4f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 18 Jun 2026 18:26:49 -0700 Subject: [PATCH 06/16] feat(files): public share links for workspace files (#5130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(files): public share links for workspace files * improvement(files): drop reserved public_share columns until used; sync audit mock * fix(files): share modal tracks authoritative saved state until toggled * feat(files): per-IP rate limit on public share endpoints * fix(files): address PR review — public CSV OOM, content cache, share FK, soft-delete filter, download anchor * fix(files): disable CSV import action in read-only preview (public share) * refactor(files): drive CSV preview import affordance off readOnly, not disableImport * fix(files): version public viewer caches by file updatedAt so edits aren't stale * fix(files): 409 (not corrupt source) when a shared generated doc has no compiled artifact * feat(files): gate public sharing behind an access-control permission --- .../api/files/public/[token]/content/route.ts | 80 + .../api/files/public/[token]/route.test.ts | 75 + .../sim/app/api/files/public/[token]/route.ts | 52 + .../[id]/files/[fileId]/share/route.test.ts | 152 + .../[id]/files/[fileId]/share/route.ts | 146 + .../app/api/workspaces/[id]/files/route.ts | 12 +- apps/sim/app/f/[token]/page.tsx | 38 + apps/sim/app/f/[token]/public-file-view.tsx | 126 + .../file-row-context-menu.tsx | 10 +- .../components/file-viewer/csv-import.ts | 7 +- .../components/file-viewer/file-viewer.tsx | 67 +- .../components/file-viewer/image-preview.tsx | 4 +- .../files/components/file-viewer/index.ts | 2 +- .../components/file-viewer/preview-panel.tsx | 12 +- .../files/components/share-modal/index.ts | 1 + .../components/share-modal/share-modal.tsx | 105 + .../workspace/[workspaceId]/files/files.tsx | 40 +- .../components/access-control.tsx | 6 + .../utils/permission-check.test.ts | 1 + .../access-control/utils/permission-check.ts | 22 + apps/sim/hooks/queries/public-shares.ts | 81 + apps/sim/hooks/queries/workspace-files.ts | 31 +- apps/sim/hooks/use-file-content-source.tsx | 42 + .../lib/api/contracts/permission-groups.ts | 1 + apps/sim/lib/api/contracts/public-shares.ts | 99 + apps/sim/lib/api/contracts/workspace-files.ts | 2 + .../copilot/tools/server/files/doc-compile.ts | 35 + apps/sim/lib/permission-groups/types.ts | 5 + apps/sim/lib/public-shares/rate-limit.ts | 45 + apps/sim/lib/public-shares/share-manager.ts | 153 + .../workspace/workspace-file-manager.ts | 3 + packages/audit/src/types.ts | 2 + packages/db/migrations/0242_public_share.sql | 18 + .../db/migrations/meta/0242_snapshot.json | 16729 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 33 + packages/testing/src/mocks/audit.mock.ts | 2 + scripts/check-api-validation-contracts.ts | 4 +- 38 files changed, 18222 insertions(+), 28 deletions(-) create mode 100644 apps/sim/app/api/files/public/[token]/content/route.ts create mode 100644 apps/sim/app/api/files/public/[token]/route.test.ts create mode 100644 apps/sim/app/api/files/public/[token]/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.test.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts create mode 100644 apps/sim/app/f/[token]/page.tsx create mode 100644 apps/sim/app/f/[token]/public-file-view.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/share-modal/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx create mode 100644 apps/sim/hooks/queries/public-shares.ts create mode 100644 apps/sim/hooks/use-file-content-source.tsx create mode 100644 apps/sim/lib/api/contracts/public-shares.ts create mode 100644 apps/sim/lib/public-shares/rate-limit.ts create mode 100644 apps/sim/lib/public-shares/share-manager.ts create mode 100644 packages/db/migrations/0242_public_share.sql create mode 100644 packages/db/migrations/meta/0242_snapshot.json diff --git a/apps/sim/app/api/files/public/[token]/content/route.ts b/apps/sim/app/api/files/public/[token]/content/route.ts new file mode 100644 index 00000000000..8c8e04a0dfe --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/content/route.ts @@ -0,0 +1,80 @@ +import { createLogger } from '@sim/logger' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getPublicFileContentContract } from '@/lib/api/contracts/public-shares' +import { parseRequest } from '@/lib/api/server' +import { resolveServableDoc } from '@/lib/copilot/tools/server/files/doc-compile' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit' +import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' +import { downloadFile } from '@/lib/uploads/core/storage-service' +import { createErrorResponse, createFileResponse, FileNotFoundError } from '@/app/api/files/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('PublicFileContentAPI') + +/** + * GET /api/files/public/[token]/content + * Public, unauthenticated bytes for a shared file. Authorized solely by an active + * share token — never by workspace membership. 404 for unknown/inactive/deleted + * shares. Disposition (inline vs attachment) is resolved from the file type by + * {@link createFileResponse}; the public page's Download button uses ``. + * + * Generated office docs are stored as source; {@link resolveServableDoc} swaps in + * their prebuilt compiled binary (read-only, never compiles). Uploaded binaries + * pass through untouched. A generated doc whose compiled artifact isn't built yet + * returns 409 rather than serving raw source under a binary content type. + */ +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + try { + const limited = await enforcePublicFileRateLimit(request, 'content') + if (limited) return limited + + const parsed = await parseRequest(getPublicFileContentContract, request, context) + if (!parsed.success) return parsed.response + const { token } = parsed.data.params + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + throw new FileNotFoundError('Not found') + } + + const { file } = resolved + const raw = await downloadFile({ key: file.key, context: 'workspace' }) + + const servable = file.workspaceId + ? await resolveServableDoc(file.workspaceId, raw, file.originalName) + : ({ kind: 'passthrough' } as const) + + if (servable.kind === 'unavailable') { + logger.info('Public shared doc not yet compiled', { token, key: file.key }) + return NextResponse.json( + { error: 'This document is still being prepared. Please try again shortly.' }, + { status: 409 } + ) + } + + const buffer = servable.kind === 'artifact' ? servable.buffer : raw + const contentType = servable.kind === 'artifact' ? servable.contentType : file.contentType + + logger.info('Public shared file served', { token, key: file.key, size: buffer.length }) + + // Revalidate every request: a shared file can be unshared, edited, or deleted, + // so the fixed token URL must never serve stale bytes from a long-lived cache. + return createFileResponse({ + buffer, + contentType, + filename: file.originalName, + cacheControl: 'private, no-cache, must-revalidate', + }) + } catch (error) { + logger.error('Error serving public shared file:', error) + if (error instanceof FileNotFoundError) { + return createErrorResponse(error) + } + return createErrorResponse(error instanceof Error ? error : new Error('Failed to serve file')) + } + } +) diff --git a/apps/sim/app/api/files/public/[token]/route.test.ts b/apps/sim/app/api/files/public/[token]/route.test.ts new file mode 100644 index 00000000000..e3d78316eba --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/route.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockResolveActiveShareByToken, mockEnforceRateLimit } = vi.hoisted(() => ({ + mockResolveActiveShareByToken: vi.fn(), + mockEnforceRateLimit: vi.fn(), +})) + +vi.mock('@/lib/public-shares/share-manager', () => ({ + resolveActiveShareByToken: mockResolveActiveShareByToken, +})) + +vi.mock('@/lib/public-shares/rate-limit', () => ({ + enforcePublicFileRateLimit: mockEnforceRateLimit, +})) + +import { NextResponse } from 'next/server' +import { GET } from '@/app/api/files/public/[token]/route' + +const params = (token = 'tok_1') => ({ params: Promise.resolve({ token }) }) +const request = (token = 'tok_1') => new NextRequest(`http://localhost/api/files/public/${token}`) + +describe('GET /api/files/public/[token]', () => { + beforeEach(() => { + vi.clearAllMocks() + mockEnforceRateLimit.mockResolvedValue(null) // allow by default + }) + + it('returns 429 when the per-IP rate limit is exceeded', async () => { + mockEnforceRateLimit.mockResolvedValueOnce( + NextResponse.json({ error: 'Too many requests. Please try again later.' }, { status: 429 }) + ) + const res = await GET(request(), params()) + expect(res.status).toBe(429) + expect(mockResolveActiveShareByToken).not.toHaveBeenCalled() + }) + + it('returns 404 for an unknown or inactive token', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce(null) + const res = await GET(request(), params()) + expect(res.status).toBe(404) + }) + + it('returns public-safe metadata (name/type/size + provenance) without leaking the key or workspace id', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce({ + share: { id: 'sh_1', token: 'tok_1' }, + file: { + id: 'wf_1', + key: 'workspace/ws/secret-key.pdf', + workspaceId: 'ws-secret', + originalName: 'report.pdf', + contentType: 'application/pdf', + size: 2048, + }, + workspaceName: 'Acme Workspace', + ownerName: 'Jane Doe', + }) + const res = await GET(request(), params()) + expect(res.status).toBe(200) + const body = await res.json() + expect(body).toEqual({ + token: 'tok_1', + name: 'report.pdf', + type: 'application/pdf', + size: 2048, + workspaceName: 'Acme Workspace', + ownerName: 'Jane Doe', + }) + expect(JSON.stringify(body)).not.toContain('secret-key') + expect(JSON.stringify(body)).not.toContain('ws-secret') + }) +}) diff --git a/apps/sim/app/api/files/public/[token]/route.ts b/apps/sim/app/api/files/public/[token]/route.ts new file mode 100644 index 00000000000..afc99e22b54 --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/route.ts @@ -0,0 +1,52 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getPublicFileContract } from '@/lib/api/contracts/public-shares' +import { parseRequest } from '@/lib/api/server' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit' +import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('PublicFileMetadataAPI') + +/** + * GET /api/files/public/[token] + * Public, unauthenticated metadata for a shared file. Returns 404 for unknown, + * inactive, or deleted shares — the existence of a file is never leaked. + */ +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + try { + const limited = await enforcePublicFileRateLimit(request, 'metadata') + if (limited) return limited + + const parsed = await parseRequest(getPublicFileContract, request, context) + if (!parsed.success) return parsed.response + const { token } = parsed.data.params + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + const { file, workspaceName, ownerName } = resolved + return NextResponse.json({ + token, + name: file.originalName, + type: file.contentType, + size: file.size, + workspaceName, + ownerName, + }) + } catch (error) { + logger.error('Error fetching public file metadata:', error) + return NextResponse.json( + { error: getErrorMessage(error, 'Failed to fetch file') }, + { status: 500 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.test.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.test.ts new file mode 100644 index 00000000000..1ee0a6c75f8 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.test.ts @@ -0,0 +1,152 @@ +/** + * @vitest-environment node + */ +import { auditMock, authMockFns, permissionsMock, permissionsMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGetWorkspaceFile, mockGetShareForResource, mockUpsertFileShare, mockValidateSharing } = + vi.hoisted(() => ({ + mockGetWorkspaceFile: vi.fn(), + mockGetShareForResource: vi.fn(), + mockUpsertFileShare: vi.fn(), + mockValidateSharing: vi.fn(), + })) + +vi.mock('@/lib/uploads/contexts/workspace', () => ({ + getWorkspaceFile: mockGetWorkspaceFile, +})) + +vi.mock('@/lib/public-shares/share-manager', () => ({ + getShareForResource: mockGetShareForResource, + upsertFileShare: mockUpsertFileShare, +})) + +vi.mock('@/ee/access-control/utils/permission-check', () => { + class PublicFileSharingNotAllowedError extends Error { + constructor() { + super('Public file sharing is not allowed based on your permission group settings') + this.name = 'PublicFileSharingNotAllowedError' + } + } + return { validatePublicFileSharing: mockValidateSharing, PublicFileSharingNotAllowedError } +}) + +vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) +vi.mock('@sim/audit', () => auditMock) + +const WS = '7727ef3f-8cf6-4686-b063-2bb006a10785' +const FILE_ID = 'wf_abc' + +import { GET, PUT } from '@/app/api/workspaces/[id]/files/[fileId]/share/route' + +const params = (id = WS, fileId = FILE_ID) => ({ params: Promise.resolve({ id, fileId }) }) + +const putRequest = (body: unknown) => + new NextRequest(`http://localhost/api/workspaces/${WS}/files/${FILE_ID}/share`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + +const getRequest = () => + new NextRequest(`http://localhost/api/workspaces/${WS}/files/${FILE_ID}/share`) + +const SHARE = { + id: 'sh_1', + token: 'tok_1', + url: 'https://sim.ai/f/tok_1', + isActive: true, + resourceType: 'file' as const, + resourceId: FILE_ID, +} + +describe('share route', () => { + beforeEach(() => { + vi.clearAllMocks() + authMockFns.mockGetSession.mockResolvedValue({ + user: { id: 'user-1', name: 'User One', email: 'u@example.com' }, + }) + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write') + mockGetWorkspaceFile.mockResolvedValue({ id: FILE_ID, name: 'report.pdf' }) + mockGetShareForResource.mockResolvedValue(SHARE) + mockUpsertFileShare.mockResolvedValue(SHARE) + mockValidateSharing.mockResolvedValue(undefined) // policy allows by default + }) + + describe('GET', () => { + it('returns 401 when unauthenticated', async () => { + authMockFns.mockGetSession.mockResolvedValueOnce(null) + const res = await GET(getRequest(), params()) + expect(res.status).toBe(401) + }) + + it('returns 403 when the caller has no workspace access', async () => { + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValueOnce(null) + const res = await GET(getRequest(), params()) + expect(res.status).toBe(403) + }) + + it('returns the share for a member', async () => { + const res = await GET(getRequest(), params()) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ share: SHARE }) + }) + }) + + describe('PUT', () => { + it('returns 403 for a read-only member', async () => { + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValueOnce('read') + const res = await PUT(putRequest({ isActive: true }), params()) + expect(res.status).toBe(403) + expect(mockUpsertFileShare).not.toHaveBeenCalled() + }) + + it('returns 404 when the file is not in the workspace', async () => { + mockGetWorkspaceFile.mockResolvedValueOnce(null) + const res = await PUT(putRequest({ isActive: true }), params()) + expect(res.status).toBe(404) + expect(mockUpsertFileShare).not.toHaveBeenCalled() + }) + + it('enables the share for a writer', async () => { + const res = await PUT(putRequest({ isActive: true }), params()) + expect(res.status).toBe(200) + expect(mockUpsertFileShare).toHaveBeenCalledWith({ + workspaceId: WS, + fileId: FILE_ID, + userId: 'user-1', + isActive: true, + }) + expect(await res.json()).toEqual({ share: SHARE }) + }) + + it('returns 403 when org access-control disables public sharing (enable)', async () => { + const { PublicFileSharingNotAllowedError } = await import( + '@/ee/access-control/utils/permission-check' + ) + mockValidateSharing.mockRejectedValueOnce(new PublicFileSharingNotAllowedError()) + const res = await PUT(putRequest({ isActive: true }), params()) + expect(res.status).toBe(403) + expect(mockUpsertFileShare).not.toHaveBeenCalled() + }) + + it('allows disabling a share even when policy disallows enabling', async () => { + mockValidateSharing.mockRejectedValue(new Error('should not be called for disable')) + const res = await PUT(putRequest({ isActive: false }), params()) + expect(res.status).toBe(200) + expect(mockValidateSharing).not.toHaveBeenCalled() + expect(mockUpsertFileShare).toHaveBeenCalledWith({ + workspaceId: WS, + fileId: FILE_ID, + userId: 'user-1', + isActive: false, + }) + }) + + it('rejects a missing isActive body', async () => { + const res = await PUT(putRequest({}), params()) + expect(res.status).toBe(400) + }) + }) +}) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts new file mode 100644 index 00000000000..9b6f523c5fd --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts @@ -0,0 +1,146 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getFileShareContract, upsertFileShareContract } from '@/lib/api/contracts/public-shares' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getShareForResource, upsertFileShare } from '@/lib/public-shares/share-manager' +import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { + PublicFileSharingNotAllowedError, + validatePublicFileSharing, +} from '@/ee/access-control/utils/permission-check' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkspaceFileShareAPI') + +/** + * GET /api/workspaces/[id]/files/[fileId]/share + * Fetch the public share state for a file (requires workspace membership). + */ +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string; fileId: string }> }) => { + const requestId = generateRequestId() + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(getFileShareContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, fileId } = parsed.data.params + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission === null) { + logger.warn( + `[${requestId}] User ${session.user.id} lacks access to workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const file = await getWorkspaceFile(workspaceId, fileId) + if (!file) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + const share = await getShareForResource('file', fileId) + return NextResponse.json({ share }) + } catch (error) { + logger.error(`[${requestId}] Error fetching file share:`, error) + return NextResponse.json( + { error: getErrorMessage(error, 'Failed to fetch share') }, + { + status: 500, + } + ) + } + } +) + +/** + * PUT /api/workspaces/[id]/files/[fileId]/share + * Enable or disable the public share for a file (requires write permission). + */ +export const PUT = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string; fileId: string }> }) => { + const requestId = generateRequestId() + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(upsertFileShareContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, fileId } = parsed.data.params + const { isActive } = parsed.data.body + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission !== 'admin' && permission !== 'write') { + logger.warn( + `[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const file = await getWorkspaceFile(workspaceId, fileId) + if (!file) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + // Enabling a public link is gated by the org's access-control policy; disabling + // is always allowed so users can still un-share after the policy is turned on. + if (isActive) { + try { + await validatePublicFileSharing(session.user.id, workspaceId) + } catch (error) { + if (error instanceof PublicFileSharingNotAllowedError) { + logger.warn(`[${requestId}] Public file sharing disabled for workspace ${workspaceId}`) + return NextResponse.json({ error: error.message }, { status: 403 }) + } + throw error + } + } + + const share = await upsertFileShare({ + workspaceId, + fileId, + userId: session.user.id, + isActive, + }) + + logger.info(`[${requestId}] ${isActive ? 'Enabled' : 'Disabled'} share for file ${fileId}`) + + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: isActive ? AuditAction.FILE_SHARED : AuditAction.FILE_SHARE_DISABLED, + resourceType: AuditResourceType.FILE, + resourceId: fileId, + resourceName: file.name, + description: `${isActive ? 'Enabled' : 'Disabled'} public share for "${file.name}"`, + request, + }) + + return NextResponse.json({ share }) + } catch (error) { + logger.error(`[${requestId}] Error updating file share:`, error) + return NextResponse.json( + { error: getErrorMessage(error, 'Failed to update share') }, + { + status: 500, + } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts index 75caa8542e7..b13a8d08b6f 100644 --- a/apps/sim/app/api/workspaces/[id]/files/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -11,6 +11,7 @@ import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' +import { getSharesForResources } from '@/lib/public-shares/share-manager' import { FileConflictError, listWorkspaceFiles, @@ -68,11 +69,20 @@ export const GET = withRouteHandler( const files = await listWorkspaceFiles(workspaceId, { scope }) + const shares = await getSharesForResources( + 'file', + files.map((file) => file.id) + ) + const filesWithShares = files.map((file) => ({ + ...file, + share: shares.get(file.id) ?? null, + })) + logger.info(`[${requestId}] Listed ${files.length} files for workspace ${workspaceId}`) return NextResponse.json({ success: true, - files, + files: filesWithShares, }) } catch (error) { logger.error(`[${requestId}] Error listing workspace files:`, error) diff --git a/apps/sim/app/f/[token]/page.tsx b/apps/sim/app/f/[token]/page.tsx new file mode 100644 index 00000000000..2e75f2f7989 --- /dev/null +++ b/apps/sim/app/f/[token]/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' +import { PublicFileView } from '@/app/f/[token]/public-file-view' + +export const dynamic = 'force-dynamic' + +/** Shared links must never be indexed by search engines. */ +export const metadata: Metadata = { + robots: { index: false, follow: false }, +} + +interface PublicFilePageProps { + params: Promise<{ token: string }> +} + +export default async function PublicFilePage({ params }: PublicFilePageProps) { + const { token } = await params + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + notFound() + } + + const { file, workspaceName, ownerName } = resolved + + return ( + + ) +} diff --git a/apps/sim/app/f/[token]/public-file-view.tsx b/apps/sim/app/f/[token]/public-file-view.tsx new file mode 100644 index 00000000000..03ea35d795e --- /dev/null +++ b/apps/sim/app/f/[token]/public-file-view.tsx @@ -0,0 +1,126 @@ +'use client' + +import { useMemo } from 'react' +import Image from 'next/image' +import Link from 'next/link' +import { Chip } from '@/components/emcn' +import { Download } from '@/components/emcn/icons' +import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import { FileViewer } from '@/app/workspace/[workspaceId]/files/components/file-viewer' +import { useBrandConfig } from '@/ee/whitelabeling' +import { type FileContentSource, FileContentSourceProvider } from '@/hooks/use-file-content-source' + +interface PublicFileViewProps { + token: string + name: string + type: string + size: number + /** Content version (the file's `updatedAt`, epoch ms) — busts the viewer's caches when the file changes. */ + version: number + workspaceName: string | null + ownerName: string | null +} + +export function PublicFileView({ + token, + name, + type, + size, + version, + workspaceName, + ownerName, +}: PublicFileViewProps) { + const contentUrl = `/api/files/public/${token}/content` + const brand = useBrandConfig() + const provenance = [workspaceName, ownerName ? `Shared by ${ownerName}` : null] + .filter(Boolean) + .join(' · ') + + // The public viewer reuses the in-app FileViewer; the content source seam swaps + // the auth-gated workspace serve URL for the token-scoped public endpoint, and a + // synthetic record carries the metadata the renderers/query keys need. `key` and + // `updatedAt` fold in the content version so the React Query caches (keyed on the + // storage key + `updatedAt`) refetch when the shared file changes — even when its + // size is unchanged. + const source = useMemo(() => ({ buildUrl: () => contentUrl }), [contentUrl]) + const file = useMemo( + () => ({ + id: token, + workspaceId: token, + name, + key: `${token}@${version}`, + path: contentUrl, + size, + type, + uploadedBy: '', + folderId: null, + uploadedAt: new Date(version), + updatedAt: new Date(version), + }), + [token, name, type, size, version, contentUrl] + ) + + return ( +
+
+
+ {!brand.logoUrl && ( + <> + + Sim + Sim + +
+ + )} +
+ {name} + {provenance ? ( + {provenance} + ) : null} +
+
+ { + const anchor = document.createElement('a') + anchor.href = contentUrl + anchor.download = name + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + }} + > + Download + +
+ +
+ + + +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx index db0853d2a3c..83b4defd0c6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -15,7 +15,7 @@ import { FolderInput, Pencil, } from '@/components/emcn' -import { Download, Trash } from '@/components/emcn/icons' +import { Download, Link, Trash } from '@/components/emcn/icons' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/files/move-options' import { renderMoveOption } from '@/app/workspace/[workspaceId]/files/move-options' @@ -28,6 +28,7 @@ interface FileRowContextMenuProps { onRename: () => void onDelete: () => void onMove?: (optionValue: string) => void + onShare?: () => void moveOptions?: MoveOptionNode[] canEdit: boolean selectedCount: number @@ -42,6 +43,7 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ onRename, onDelete, onMove, + onShare, moveOptions, canEdit, selectedCount, @@ -85,6 +87,12 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ Rename )} + {!isMultiSelect && onShare && ( + + + Share + + )} {onMove && moveOptions && moveOptions.length > 0 && ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts index d852820bfc1..eae5e438133 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts @@ -19,7 +19,8 @@ export type CsvImportFileDescriptor = Pick export function useCsvTruncationImport( workspaceId: string, file: CsvImportFileDescriptor, - truncated: boolean + truncated: boolean, + readOnly = false ) { const router = useRouter() const importFile = useImportFileAsTable() @@ -58,11 +59,11 @@ export function useCsvTruncationImport( // Surface the cap as a warning toast with an import action, once per file. const notifiedKeyRef = useRef(null) useEffect(() => { - if (!truncated || notifiedKeyRef.current === file.key) return + if (readOnly || !truncated || notifiedKeyRef.current === file.key) return notifiedKeyRef.current = file.key toast.warning(`Showing the first ${CSV_PREVIEW_MAX_ROWS.toLocaleString()} rows`, { description: 'Import this file as a table to view all of its rows.', action: { label: 'Import as a table', onClick: importAsTable }, }) - }, [truncated, file.key, importAsTable]) + }, [readOnly, truncated, file.key, importAsTable]) } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index d3c6fb21ece..5ca650e971a 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -6,7 +6,7 @@ import { Music } from 'lucide-react' import dynamic from 'next/dynamic' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { getFileExtension } from '@/lib/uploads/utils/file-utils' -import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files' +import { useWorkspaceFileBinary, useWorkspaceFileContent } from '@/hooks/queries/workspace-files' import { resolveFileCategory } from './file-category' import type { StreamingMode } from './text-editor-state' import { useDocPreviewBinary } from './use-doc-preview-binary' @@ -18,7 +18,7 @@ import { DocxPreview } from './docx-preview' import { ImagePreview } from './image-preview' import type { PdfDocumentSource } from './pdf-viewer' import { PptxPreview } from './pptx-preview' -import { resolvePreviewType } from './preview-panel' +import { PreviewPanel, resolvePreviewType } from './preview-panel' import { PREVIEW_LOADING_OVERLAY, PreviewError, @@ -72,6 +72,12 @@ interface FileViewerProps { file: WorkspaceFileRecord workspaceId: string canEdit: boolean + /** + * Render a read-only preview with no editing affordances. Text files render + * through {@link PreviewPanel} (or a plain `
`) instead of the editable
+   * {@link TextEditor}. Used by the public share page.
+   */
+  readOnly?: boolean
   previewMode?: PreviewMode
   autoFocus?: boolean
   onDirtyChange?: (isDirty: boolean) => void
@@ -87,6 +93,7 @@ export function FileViewer({
   file,
   workspaceId,
   canEdit,
+  readOnly = false,
   previewMode,
   autoFocus,
   onDirtyChange,
@@ -100,6 +107,15 @@ export function FileViewer({
   const category = resolveFileCategory(file.type, file.name)
 
   if (category === 'text-editable') {
+    if (readOnly) {
+      // ReadOnlyTextPreview loads the whole file as text; a large CSV would OOM the
+      // browser. CsvTablePreview's streamed fallback is workspace-only, so on the
+      // read-only public path a large CSV is download-only.
+      if (isCsvStreamOnly(file)) {
+        return 
+      }
+      return 
+    }
     // A large CSV can't be loaded whole into the editor (the browser OOMs on the full text).
     // Render a streamed, read-only preview of the first rows + an "Import as a table" path instead.
     if (isCsvStreamOnly(file)) {
@@ -155,6 +171,53 @@ export function FileViewer({
   return 
 }
 
+/**
+ * Read-only text/markdown/code preview. Renders rich types (markdown, csv, svg,
+ * mermaid, html) through {@link PreviewPanel} and plain text/code in a `
`.
+ * Fetches content through the active content source, so it works for both
+ * workspace files and public share links.
+ */
+const ReadOnlyTextPreview = memo(function ReadOnlyTextPreview({
+  file,
+  workspaceId,
+}: {
+  file: WorkspaceFileRecord
+  workspaceId: string
+}) {
+  const {
+    data: content,
+    isLoading,
+    error,
+  } = useWorkspaceFileContent(workspaceId, file.id, file.key)
+
+  const resolvedError = resolvePreviewError((error as Error | null) ?? null, null)
+  if (resolvedError) return 
+  if (isLoading || content == null) return 
+
+  if (resolvePreviewType(file.type, file.name)) {
+    return (
+      
+ +
+ ) + } + + return ( +
+
+        {content}
+      
+
+ ) +}) + const IframePreview = memo(function IframePreview({ file, workspaceId, diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx index 3e04267a8ac..0bac2180bed 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx @@ -2,10 +2,12 @@ import { memo } from 'react' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import { useFileContentSource } from '@/hooks/use-file-content-source' import { ZoomablePreview } from './zoomable-preview' export const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileRecord }) { - const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` + const source = useFileContentSource() + const serveUrl = source.buildUrl(file.key) return ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts index 61991651faa..d41898d004a 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts @@ -1,4 +1,4 @@ export { resolveFileCategory } from './file-category' export type { PreviewMode } from './file-viewer' export { FileViewer, isCsvStreamOnly, isPreviewable, isTextEditable } from './file-viewer' -export { RICH_PREVIEWABLE_EXTENSIONS } from './preview-panel' +export { PreviewPanel, RICH_PREVIEWABLE_EXTENSIONS, resolvePreviewType } from './preview-panel' diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx index 127d571271f..809b90974f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx @@ -83,6 +83,12 @@ interface PreviewPanelProps { isStreaming?: boolean disableAutoScroll?: boolean onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void + /** + * Read-only surface (e.g. the public share page) — disables interactive + * affordances such as the CSV "Import as a table" action, which needs an + * authenticated workspace import. + */ + readOnly?: boolean } export const PreviewPanel = memo(function PreviewPanel({ @@ -94,6 +100,7 @@ export const PreviewPanel = memo(function PreviewPanel({ isStreaming, disableAutoScroll, onCheckboxToggle, + readOnly, }: PreviewPanelProps) { const previewType = resolvePreviewType(mimeType, filename) @@ -113,6 +120,7 @@ export const PreviewPanel = memo(function PreviewPanel({ content={content} workspaceId={workspaceId} file={{ key: fileKey, name: filename }} + readOnly={readOnly} /> ) if (previewType === 'svg') return @@ -1167,13 +1175,15 @@ const CsvPreview = memo(function CsvPreview({ content, workspaceId, file, + readOnly, }: { content: string workspaceId: string file: CsvImportFileDescriptor + readOnly?: boolean }) { const { headers, rows, truncated } = useMemo(() => parseCsv(content), [content]) - useCsvTruncationImport(workspaceId, file, truncated) + useCsvTruncationImport(workspaceId, file, truncated, readOnly) if (headers.length === 0) { return ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/index.ts b/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/index.ts new file mode 100644 index 00000000000..18519acde4b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/index.ts @@ -0,0 +1 @@ +export { ShareModal } from './share-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx new file mode 100644 index 00000000000..57596a8aa3f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx @@ -0,0 +1,105 @@ +'use client' + +import { useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + ChipSwitch, +} from '@/components/emcn' +import { Link } from '@/components/emcn/icons' +import type { ShareRecord } from '@/lib/api/contracts/public-shares' +import { useFileShare, useUpsertFileShare } from '@/hooks/queries/public-shares' +import { usePermissionConfig } from '@/hooks/use-permission-config' + +interface ShareModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId: string + fileId: string + fileName: string + /** Share state already known from the file row, used as the initial value to avoid flicker. */ + initialShare?: ShareRecord | null +} + +const VISIBILITY_OPTIONS = [ + { value: 'private', label: 'Private' }, + { value: 'public', label: 'Anyone with link' }, +] + +export function ShareModal({ + open, + onOpenChange, + workspaceId, + fileId, + fileName, + initialShare, +}: ShareModalProps) { + const { data: share } = useFileShare(workspaceId, fileId, { enabled: open }) + const { config: permissionConfig } = usePermissionConfig() + const upsertShare = useUpsertFileShare() + + const saved = share ?? initialShare ?? null + const savedActive = saved?.isActive ?? false + + // Org access-control policy can disable enabling new public links (the route is the + // source of truth; this just reflects it). Disabling an existing share stays allowed. + const enableBlockedByPolicy = permissionConfig.disablePublicFileSharing && !savedActive + + // `null` until the user toggles, so the switch always reflects the authoritative + // saved state (which may resolve after mount via useFileShare) instead of a stale + // initial snapshot — otherwise a Save could silently flip sharing the wrong way. + const [draftActive, setDraftActive] = useState(null) + const effectiveActive = draftActive ?? savedActive + const isDirty = draftActive !== null && draftActive !== savedActive + + const handleSave = () => { + upsertShare.mutate( + { workspaceId, fileId, isActive: effectiveActive }, + { onSuccess: () => onOpenChange(false) } + ) + } + + return ( + + onOpenChange(false)}> + Share file + + + + setDraftActive(value === 'public')} + options={VISIBILITY_OPTIONS} + aria-label='File access' + /> + + {saved?.isActive ? ( + + ) : null} + + onOpenChange(false)} + primaryAction={{ + label: upsertShare.isPending ? 'Saving...' : 'Save', + onClick: handleSave, + disabled: !isDirty || upsertShare.isPending || (effectiveActive && enableBlockedByPolicy), + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 53e1ba66cf5..a56e008a857 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -22,7 +22,7 @@ import { toast, Upload, } from '@/components/emcn' -import { Download } from '@/components/emcn/icons' +import { Download, Link } from '@/components/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import { captureEvent } from '@/lib/posthog/client' import { triggerFileDownload } from '@/lib/uploads/client/download' @@ -70,6 +70,7 @@ import { isTextEditable, } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { FilesListContextMenu } from '@/app/workspace/[workspaceId]/files/components/files-list-context-menu' +import { ShareModal } from '@/app/workspace/[workspaceId]/files/components/share-modal' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/files/move-options' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' @@ -269,6 +270,7 @@ export function Files() { folderIds: string[] name: string } | null>(null) + const [shareFileId, setShareFileId] = useState(null) const listRename = useInlineRename({ onSave: (rowId, name) => { @@ -296,6 +298,18 @@ export function Files() { const selectedFileRef = useRef(selectedFile) selectedFileRef.current = selectedFile + const shareFile = shareFileId ? (files.find((f) => f.id === shareFileId) ?? null) : null + const shareModal = shareFile ? ( + !open && setShareFileId(null)} + workspaceId={workspaceId} + fileId={shareFile.id} + fileName={shareFile.name} + initialShare={shareFile.share ?? null} + /> + ) : null + const folderById = useMemo(() => new Map(folders.map((folder) => [folder.id, folder])), [folders]) const currentFolder = currentFolderId ? (folderById.get(currentFolderId) ?? null) : null @@ -978,6 +992,11 @@ export function Files() { } }, []) + const handleShareSelected = useCallback(() => { + const file = selectedFileRef.current + if (file) setShareFileId(file.id) + }, []) + const handleBulkDelete = useCallback(() => { if (selectedFileIds.length === 0 && selectedFolderIds.length === 0) return setDeleteTarget({ @@ -1055,6 +1074,7 @@ export function Files() { ...(canEdit ? [ { label: 'Rename', icon: Pencil, onClick: handleStartHeaderRename }, + { label: 'Share', icon: Link, onClick: handleShareSelected }, { label: 'Delete', icon: Trash, onClick: handleDeleteSelected }, ] : []), @@ -1071,6 +1091,7 @@ export function Files() { headerRename.editValue, handleStartHeaderRename, handleDownloadSelected, + handleShareSelected, handleDeleteSelected, ]) @@ -1220,6 +1241,12 @@ export function Files() { closeContextMenu() }, [listRename.startRename, closeContextMenu]) + const handleContextMenuShare = useCallback(() => { + const item = contextMenuItemRef.current + if (item?.kind === 'file') setShareFileId(item.file.id) + closeContextMenu() + }, [closeContextMenu]) + const handleContextMenuDelete = useCallback(() => { const item = contextMenuItemRef.current if (!item) return @@ -1448,6 +1475,11 @@ export function Files() { }, ...(canEdit ? [ + { + text: 'Share', + icon: Link, + onSelect: handleShareSelected, + }, { text: 'Delete', icon: Trash, @@ -1466,6 +1498,7 @@ export function Files() { handleTogglePreview, handleSave, handleDownloadSelected, + handleShareSelected, handleDeleteSelected, ]) @@ -1875,6 +1908,8 @@ export function Files() { onDelete={handleDelete} isPending={deleteFile.isPending || bulkArchiveItems.isPending} /> + + {shareModal} ) } @@ -1956,6 +1991,7 @@ export function Files() { onRename={handleContextMenuRename} onDelete={handleContextMenuDelete} onMove={handleContextMenuMove} + onShare={canEdit ? handleContextMenuShare : undefined} moveOptions={contextMenuMoveOptions} canEdit={canEdit} selectedCount={selectedRowIds.size} @@ -1971,6 +2007,8 @@ export function Files() { isPending={deleteFile.isPending || bulkArchiveItems.isPending} /> + {shareModal} + { + const config = await getUserPermissionConfig(userId, workspaceId) + if (config?.disablePublicFileSharing) { + throw new PublicFileSharingNotAllowedError() + } +} + /** * Org-addressed variant of {@link getUserPermissionConfig}. Use when only the * organization is known (e.g. organization-level invitations); resolves the diff --git a/apps/sim/hooks/queries/public-shares.ts b/apps/sim/hooks/queries/public-shares.ts new file mode 100644 index 00000000000..a32cf0a968f --- /dev/null +++ b/apps/sim/hooks/queries/public-shares.ts @@ -0,0 +1,81 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { toast } from '@/components/emcn' +import { requestJson } from '@/lib/api/client/request' +import { + getFileShareContract, + type ShareRecord, + type UpsertFileShareBody, + upsertFileShareContract, +} from '@/lib/api/contracts/public-shares' +import { workspaceFilesKeys } from '@/hooks/queries/workspace-files' + +/** + * Query key factories for public shares + */ +export const shareKeys = { + all: ['publicShares'] as const, + details: () => [...shareKeys.all, 'detail'] as const, + detail: (workspaceId: string, fileId: string) => + [...shareKeys.details(), workspaceId, fileId] as const, +} + +async function fetchFileShare( + workspaceId: string, + fileId: string, + signal?: AbortSignal +): Promise { + const data = await requestJson(getFileShareContract, { + params: { id: workspaceId, fileId }, + signal, + }) + return data.share +} + +export function useFileShare(workspaceId: string, fileId: string, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: shareKeys.detail(workspaceId, fileId), + queryFn: ({ signal }) => fetchFileShare(workspaceId, fileId, signal), + enabled: Boolean(workspaceId) && Boolean(fileId) && (options?.enabled ?? true), + staleTime: 30 * 1000, + }) +} + +interface UpsertFileShareVariables extends UpsertFileShareBody { + workspaceId: string + fileId: string +} + +export function useUpsertFileShare() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ workspaceId, fileId, isActive }: UpsertFileShareVariables) => + requestJson(upsertFileShareContract, { + params: { id: workspaceId, fileId }, + body: { isActive }, + }), + onSuccess: (data, { workspaceId, fileId, isActive }) => { + queryClient.setQueryData(shareKeys.detail(workspaceId, fileId), data.share) + queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.workspaceLists(workspaceId) }) + if (!isActive) { + toast.success('Sharing turned off') + return + } + const { url } = data.share + toast.success('Public link enabled', { + description: url, + action: { + label: 'Copy link', + onClick: () => { + navigator.clipboard.writeText(url).then( + () => toast.success('Link copied'), + () => toast.error('Failed to copy link') + ) + }, + }, + }) + }, + onError: (error) => { + toast.error(error.message) + }, + }) +} diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 6d97754ba42..b3a0c1bd589 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -22,6 +22,7 @@ import { } from '@/lib/uploads/client/direct-upload' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import type { UserFile } from '@/executor/types' +import { useFileContentSource } from '@/hooks/use-file-content-source' const logger = createLogger('WorkspaceFilesQuery') @@ -114,16 +115,11 @@ export function useWorkspaceFiles( } /** - * Fetch file content as text via the serve URL + * Fetch file content as text via a content-source URL */ -async function fetchWorkspaceFileContent( - key: string, - signal?: AbortSignal, - raw?: boolean -): Promise { - const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace&t=${Date.now()}${raw ? '&raw=1' : ''}` +async function fetchWorkspaceFileContent(url: string, signal?: AbortSignal): Promise { // boundary-raw-fetch: binary/text download, response is not JSON - const response = await fetch(serveUrl, { signal, cache: 'no-store' }) + const response = await fetch(url, { signal, cache: 'no-store' }) if (!response.ok) { throw new Error('Failed to fetch file content') @@ -143,9 +139,11 @@ export function useWorkspaceFileContent( key: string, raw?: boolean ) { + const source = useFileContentSource() return useQuery({ queryKey: workspaceFilesKeys.content(workspaceId, fileId, raw ? 'raw' : 'text', key), - queryFn: ({ signal }) => fetchWorkspaceFileContent(key, signal, raw), + queryFn: ({ signal }) => + fetchWorkspaceFileContent(source.buildUrl(key, { raw, bust: true }), signal), enabled: !!workspaceId && !!fileId && !!key, staleTime: 30 * 1000, refetchOnWindowFocus: 'always', @@ -177,16 +175,13 @@ export class DocNotReadyError extends Error { * so the query keeps polling. */ async function fetchWorkspaceFileBinary( - key: string, + url: string, version: string | number | undefined, signal?: AbortSignal ): Promise { - const cacheParam = - version != null ? `v=${encodeURIComponent(String(version))}` : `t=${Date.now()}` - const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace&${cacheParam}` const init: RequestInit = version != null ? { signal } : { signal, cache: 'no-store' } // boundary-raw-fetch: binary download consumed as ArrayBuffer - const response = await fetch(serveUrl, init) + const response = await fetch(url, init) if (response.status === 409) throw new DocNotReadyError() if (!response.ok) throw new Error('Failed to fetch file content') return response.arrayBuffer() @@ -210,12 +205,18 @@ export function useWorkspaceFileBinary( key: string, options?: { enabled?: boolean; version?: string | number } ) { + const source = useFileContentSource() return useQuery({ queryKey: options?.version != null ? [...workspaceFilesKeys.content(workspaceId, fileId, 'binary', key), options.version] : workspaceFilesKeys.content(workspaceId, fileId, 'binary', key), - queryFn: ({ signal }) => fetchWorkspaceFileBinary(key, options?.version, signal), + queryFn: ({ signal }) => + fetchWorkspaceFileBinary( + source.buildUrl(key, { version: options?.version, bust: true }), + options?.version, + signal + ), // Callers gate this on a readiness signal (e.g. the file has committed // content) so we don't 409-poll the serve route for a generated doc whose // compiled artifact hasn't been written yet — the doc is fetched once, when diff --git a/apps/sim/hooks/use-file-content-source.tsx b/apps/sim/hooks/use-file-content-source.tsx new file mode 100644 index 00000000000..ee09819d7c8 --- /dev/null +++ b/apps/sim/hooks/use-file-content-source.tsx @@ -0,0 +1,42 @@ +'use client' + +import { createContext, useContext } from 'react' + +export interface FileContentUrlOptions { + /** Request the uncompiled source instead of the rendered/compiled bytes. */ + raw?: boolean + /** Content version (e.g. the record's `updatedAt`) — makes the URL cacheable/immutable. */ + version?: string | number + /** Append a timestamp cache-buster when there is no `version`. */ + bust?: boolean +} + +/** + * Seam for "where do a file's bytes come from". The in-app viewer resolves the + * auth-gated workspace serve URL; the public share page swaps in a token-scoped + * URL. Renderers and the binary/text query hooks build their fetch URL through + * this source so the same components work in both contexts. + */ +export interface FileContentSource { + buildUrl: (key: string, opts?: FileContentUrlOptions) => string +} + +/** Default source: the auth-gated workspace serve URL (the historical behavior). */ +export const workspaceFileContentSource: FileContentSource = { + buildUrl: (key, opts) => { + const base = `/api/files/serve/${encodeURIComponent(key)}?context=workspace` + const params: string[] = [] + if (opts?.version != null) params.push(`v=${encodeURIComponent(String(opts.version))}`) + else if (opts?.bust) params.push(`t=${Date.now()}`) + if (opts?.raw) params.push('raw=1') + return params.length > 0 ? `${base}&${params.join('&')}` : base + }, +} + +const FileContentSourceContext = createContext(workspaceFileContentSource) + +export const FileContentSourceProvider = FileContentSourceContext.Provider + +export function useFileContentSource(): FileContentSource { + return useContext(FileContentSourceContext) +} diff --git a/apps/sim/lib/api/contracts/permission-groups.ts b/apps/sim/lib/api/contracts/permission-groups.ts index 2ea7d651ff2..950307ed915 100644 --- a/apps/sim/lib/api/contracts/permission-groups.ts +++ b/apps/sim/lib/api/contracts/permission-groups.ts @@ -21,6 +21,7 @@ export const permissionGroupFullConfigSchema = z.object({ disableSkills: z.boolean(), disableInvitations: z.boolean(), disablePublicApi: z.boolean(), + disablePublicFileSharing: z.boolean(), hideDeployApi: z.boolean(), hideDeployMcp: z.boolean(), hideDeployA2a: z.boolean(), diff --git a/apps/sim/lib/api/contracts/public-shares.ts b/apps/sim/lib/api/contracts/public-shares.ts new file mode 100644 index 00000000000..30f8015ddd0 --- /dev/null +++ b/apps/sim/lib/api/contracts/public-shares.ts @@ -0,0 +1,99 @@ +import { z } from 'zod' +import { workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { defineRouteContract } from '@/lib/api/contracts/types' + +export const shareResourceTypeSchema = z.enum(['file', 'folder']) + +/** + * Public-safe representation of a `public_share` row. Never carries the + * underlying storage key. + */ +export const shareRecordSchema = z.object({ + id: z.string(), + token: z.string(), + url: z.string(), + isActive: z.boolean(), + resourceType: shareResourceTypeSchema, + resourceId: z.string(), +}) + +export type ShareRecord = z.output + +const fileShareParamsSchema = z.object({ + id: workspaceIdSchema, + fileId: z.string().min(1, 'File ID is required'), +}) + +export const upsertFileShareBodySchema = z.object({ + isActive: z.boolean(), +}) + +export type UpsertFileShareBody = z.input + +const getFileShareResponseSchema = z.object({ + share: shareRecordSchema.nullable(), +}) + +export type GetFileShareResponse = z.output + +export const getFileShareContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/files/[fileId]/share', + params: fileShareParamsSchema, + response: { + mode: 'json', + schema: getFileShareResponseSchema, + }, +}) + +const upsertFileShareResponseSchema = z.object({ + share: shareRecordSchema, +}) + +export type UpsertFileShareResponse = z.output + +export const upsertFileShareContract = defineRouteContract({ + method: 'PUT', + path: '/api/workspaces/[id]/files/[fileId]/share', + params: fileShareParamsSchema, + body: upsertFileShareBodySchema, + response: { + mode: 'json', + schema: upsertFileShareResponseSchema, + }, +}) + +export const publicFileTokenParamsSchema = z.object({ + token: z.string().min(1, 'Token is required'), +}) + +const publicFileMetadataSchema = z.object({ + token: z.string(), + name: z.string(), + type: z.string(), + size: z.number(), + workspaceName: z.string().nullable(), + ownerName: z.string().nullable(), +}) + +export type PublicFileMetadata = z.output + +export const getPublicFileContract = defineRouteContract({ + method: 'GET', + path: '/api/files/public/[token]', + params: publicFileTokenParamsSchema, + response: { + mode: 'json', + schema: publicFileMetadataSchema, + }, +}) + +/** Binary stream of the shared file's bytes. Authorized solely by an active token. */ +export const getPublicFileContentContract = defineRouteContract({ + method: 'GET', + path: '/api/files/public/[token]/content', + params: publicFileTokenParamsSchema, + response: { + mode: 'binary', + }, +}) diff --git a/apps/sim/lib/api/contracts/workspace-files.ts b/apps/sim/lib/api/contracts/workspace-files.ts index 7ea83ca4c5f..c7f1f6d5366 100644 --- a/apps/sim/lib/api/contracts/workspace-files.ts +++ b/apps/sim/lib/api/contracts/workspace-files.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { shareRecordSchema } from '@/lib/api/contracts/public-shares' import { defineRouteContract } from '@/lib/api/contracts/types' export const workspaceFileScopeSchema = z.enum(['active', 'archived', 'all']) @@ -49,6 +50,7 @@ export const workspaceFileRecordSchema = z.object({ uploadedAt: z.coerce.date(), updatedAt: z.coerce.date(), storageContext: z.enum(['workspace', 'mothership']).optional(), + share: shareRecordSchema.nullable().optional(), }) const workspaceFileSuccessSchema = z.object({ diff --git a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts index f61c917c9b7..1429fc705d1 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts @@ -414,3 +414,38 @@ export async function loadCompiledDocByExt( const buffer = await loadCompiledDoc(workspaceId, source, fmt.ext) return buffer ? { buffer, contentType: fmt.contentType } : null } + +const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]) +const PDF_MAGIC = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]) // %PDF- + +function bufferStartsWith(buffer: Buffer, magic: Buffer): boolean { + return buffer.length >= magic.length && buffer.subarray(0, magic.length).equals(magic) +} + +/** + * How a read-only consumer (e.g. the public share route) should serve a stored doc + * WITHOUT compiling: + * - `passthrough` — serve the raw stored bytes as-is (a non-doc file, or an uploaded + * binary that already carries its format magic). + * - `artifact` — serve this prebuilt content-addressed compiled binary. + * - `unavailable` — a generated doc stored as source whose compiled artifact does + * not exist yet; the raw bytes are source, so serving them under the file's binary + * content type would be corrupt. The caller should signal "not ready" instead. + */ +export type ServableDoc = + | { kind: 'passthrough' } + | { kind: 'artifact'; buffer: Buffer; contentType: string } + | { kind: 'unavailable' } + +export async function resolveServableDoc( + workspaceId: string, + storedBytes: Buffer, + fileName: string +): Promise { + const fmt = await getE2BDocFormat(fileName) + if (!fmt) return { kind: 'passthrough' } + const magic = fmt.ext === 'pdf' ? PDF_MAGIC : ZIP_MAGIC + if (bufferStartsWith(storedBytes, magic)) return { kind: 'passthrough' } + const artifact = await loadCompiledDocByExt(workspaceId, storedBytes.toString('utf-8'), fmt.ext) + return artifact ? { kind: 'artifact', ...artifact } : { kind: 'unavailable' } +} diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index 2853a82a2d3..809589d679f 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -31,6 +31,7 @@ export const permissionGroupConfigSchema = z.object({ disableSkills: z.boolean().optional(), disableInvitations: z.boolean().optional(), disablePublicApi: z.boolean().optional(), + disablePublicFileSharing: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), @@ -60,6 +61,7 @@ export interface PermissionGroupConfig { disableSkills: boolean disableInvitations: boolean disablePublicApi: boolean + disablePublicFileSharing: boolean hideDeployApi: boolean hideDeployMcp: boolean hideDeployA2a: boolean @@ -85,6 +87,7 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { disableSkills: false, disableInvitations: false, disablePublicApi: false, + disablePublicFileSharing: false, hideDeployApi: false, hideDeployMcp: false, hideDeployA2a: false, @@ -120,6 +123,8 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf disableSkills: typeof c.disableSkills === 'boolean' ? c.disableSkills : false, disableInvitations: typeof c.disableInvitations === 'boolean' ? c.disableInvitations : false, disablePublicApi: typeof c.disablePublicApi === 'boolean' ? c.disablePublicApi : false, + disablePublicFileSharing: + typeof c.disablePublicFileSharing === 'boolean' ? c.disablePublicFileSharing : false, hideDeployApi: typeof c.hideDeployApi === 'boolean' ? c.hideDeployApi : false, hideDeployMcp: typeof c.hideDeployMcp === 'boolean' ? c.hideDeployMcp : false, hideDeployA2a: typeof c.hideDeployA2a === 'boolean' ? c.hideDeployA2a : false, diff --git a/apps/sim/lib/public-shares/rate-limit.ts b/apps/sim/lib/public-shares/rate-limit.ts new file mode 100644 index 00000000000..60f7223a60d --- /dev/null +++ b/apps/sim/lib/public-shares/rate-limit.ts @@ -0,0 +1,45 @@ +import { NextResponse } from 'next/server' +import { RateLimiter, type TokenBucketConfig } from '@/lib/core/rate-limiter' +import { getClientIp } from '@/lib/core/utils/request' + +const rateLimiter = new RateLimiter() + +/** Metadata reads are cheap (one indexed lookup) — generous per-IP budget. */ +const METADATA_RATE_LIMIT: TokenBucketConfig = { + maxTokens: 120, + refillRate: 120, + refillIntervalMs: 60_000, +} + +/** Content reads stream bytes from storage (S3 egress) — tighter per-IP budget. */ +const CONTENT_RATE_LIMIT: TokenBucketConfig = { + maxTokens: 60, + refillRate: 60, + refillIntervalMs: 60_000, +} + +/** + * Per-IP rate limit for the unauthenticated public share endpoints, returning a + * `429` response when exceeded (or `null` to proceed). The token is unguessable, + * so this defends a *known* link against hammering (DoS / S3 egress) rather than + * enumeration. Fails open on storage errors (availability over strictness), + * matching the chat public route. + */ +export async function enforcePublicFileRateLimit( + request: { headers: { get(name: string): string | null } }, + scope: 'metadata' | 'content' +): Promise { + const ip = getClientIp(request) + const config = scope === 'content' ? CONTENT_RATE_LIMIT : METADATA_RATE_LIMIT + const result = await rateLimiter.checkRateLimitDirect(`public-file:${scope}:${ip}`, config) + if (result.allowed) return null + + const headers = + result.retryAfterMs != null + ? { 'Retry-After': String(Math.ceil(result.retryAfterMs / 1000)) } + : undefined + return NextResponse.json( + { error: 'Too many requests. Please try again later.' }, + { status: 429, headers } + ) +} diff --git a/apps/sim/lib/public-shares/share-manager.ts b/apps/sim/lib/public-shares/share-manager.ts new file mode 100644 index 00000000000..ae502bd3333 --- /dev/null +++ b/apps/sim/lib/public-shares/share-manager.ts @@ -0,0 +1,153 @@ +import { db } from '@sim/db' +import { publicShare, user, workspace, workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId, generateShortId } from '@sim/utils/id' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import type { z } from 'zod' +import type { ShareRecord, shareResourceTypeSchema } from '@/lib/api/contracts/public-shares' +import { getBaseUrl } from '@/lib/core/utils/urls' + +const logger = createLogger('PublicShareManager') + +type ShareResourceType = z.infer + +type PublicShareRow = typeof publicShare.$inferSelect + +/** Public share URL for a token: `{baseUrl}/f/{token}`. */ +export function buildShareUrl(token: string): string { + return `${getBaseUrl()}/f/${token}` +} + +function mapShareRecord(row: PublicShareRow): ShareRecord { + return { + id: row.id, + token: row.token, + url: buildShareUrl(row.token), + isActive: row.isActive, + resourceType: row.resourceType as ShareResourceType, + resourceId: row.resourceId, + } +} + +export async function getShareForResource( + resourceType: ShareResourceType, + resourceId: string +): Promise { + const [row] = await db + .select() + .from(publicShare) + .where(and(eq(publicShare.resourceType, resourceType), eq(publicShare.resourceId, resourceId))) + .limit(1) + + return row ? mapShareRecord(row) : null +} + +/** + * Batch-fetch shares for many resources of the same type, keyed by `resourceId`. + * Used to enrich the files list without an N+1 query. + */ +export async function getSharesForResources( + resourceType: ShareResourceType, + resourceIds: string[] +): Promise> { + const result = new Map() + if (resourceIds.length === 0) return result + + const rows = await db + .select() + .from(publicShare) + .where( + and(eq(publicShare.resourceType, resourceType), inArray(publicShare.resourceId, resourceIds)) + ) + + for (const row of rows) { + result.set(row.resourceId, mapShareRecord(row)) + } + return result +} + +interface UpsertFileShareInput { + workspaceId: string + fileId: string + userId: string + isActive: boolean +} + +/** + * Enable or disable the public share for a file. First enable inserts a row with + * a fresh unguessable token; subsequent calls flip `isActive` and keep the token + * stable (so an existing link resolves again after re-enable). + */ +export async function upsertFileShare({ + workspaceId, + fileId, + userId, + isActive, +}: UpsertFileShareInput): Promise { + const [row] = await db + .insert(publicShare) + .values({ + id: generateId(), + resourceType: 'file', + resourceId: fileId, + workspaceId, + createdBy: userId, + token: generateShortId(), + isActive, + }) + .onConflictDoUpdate({ + target: [publicShare.resourceType, publicShare.resourceId], + set: { isActive, updatedAt: new Date() }, + }) + .returning() + + logger.info('Upserted file share', { fileId, workspaceId, isActive, token: row.token }) + return mapShareRecord(row) +} + +/** + * Resolve a public token to its active share and the underlying (non-deleted) + * file. Returns null if the token is unknown, the share is inactive, or the file + * is gone. The caller treats null as a 404 — the existence of a file is never + * leaked through this path. + */ +export interface ResolvedShare { + share: PublicShareRow + file: typeof workspaceFiles.$inferSelect + /** Owning workspace name, for provenance on the public page. */ + workspaceName: string | null + /** Display name of the file's uploader. */ + ownerName: string | null +} + +export async function resolveActiveShareByToken(token: string): Promise { + const [row] = await db + .select({ + share: publicShare, + file: workspaceFiles, + workspaceName: workspace.name, + ownerName: user.name, + }) + .from(publicShare) + .innerJoin(workspaceFiles, eq(workspaceFiles.id, publicShare.resourceId)) + .leftJoin(workspace, eq(workspace.id, workspaceFiles.workspaceId)) + .leftJoin(user, eq(user.id, workspaceFiles.userId)) + .where( + and( + eq(publicShare.token, token), + eq(publicShare.isActive, true), + eq(publicShare.resourceType, 'file'), + isNull(workspaceFiles.deletedAt) + ) + ) + .limit(1) + + if (!row) return null + + return { + share: row.share, + file: row.file, + workspaceName: row.workspaceName, + ownerName: row.ownerName, + } +} diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 781c1f377f7..f68d919a91f 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -10,6 +10,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' import { and, eq, isNull, sql } from 'drizzle-orm' +import type { ShareRecord } from '@/lib/api/contracts/public-shares' import { checkStorageQuota, decrementStorageUsage, @@ -72,6 +73,8 @@ export interface WorkspaceFileRecord { updatedAt: Date /** Pass-through to `downloadFile` when not default `workspace` (e.g. chat mothership uploads). */ storageContext?: 'workspace' | 'mothership' + /** Public share state, attached at the API boundary. `null` when never shared. */ + share?: ShareRecord | null } interface ListWorkspaceFilesOptions { diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 7eccc1f757f..611c2fd509b 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -64,6 +64,8 @@ export const AuditAction = { FILE_DELETED: 'file.deleted', FILE_RESTORED: 'file.restored', FILE_MOVED: 'file.moved', + FILE_SHARED: 'file.shared', + FILE_SHARE_DISABLED: 'file.share_disabled', // Folders FOLDER_CREATED: 'folder.created', diff --git a/packages/db/migrations/0242_public_share.sql b/packages/db/migrations/0242_public_share.sql new file mode 100644 index 00000000000..94658e4bee1 --- /dev/null +++ b/packages/db/migrations/0242_public_share.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public_share" ( + "id" text PRIMARY KEY NOT NULL, + "resource_type" text NOT NULL, + "resource_id" text NOT NULL, + "workspace_id" text NOT NULL, + "created_by" text, + "token" text NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "public_share" ADD CONSTRAINT "public_share_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "public_share" ADD CONSTRAINT "public_share_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "public_share_token_unique" ON "public_share" USING btree ("token");--> statement-breakpoint +CREATE UNIQUE INDEX "public_share_resource_unique" ON "public_share" USING btree ("resource_type","resource_id");--> statement-breakpoint +CREATE INDEX "public_share_resource_id_idx" ON "public_share" USING btree ("resource_id");--> statement-breakpoint +CREATE INDEX "public_share_workspace_id_idx" ON "public_share" USING btree ("workspace_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0242_snapshot.json b/packages/db/migrations/meta/0242_snapshot.json new file mode 100644 index 00000000000..58a105272ce --- /dev/null +++ b/packages/db/migrations/meta/0242_snapshot.json @@ -0,0 +1,16729 @@ +{ + "id": "78b8f3d4-c24c-4303-89ec-8ae29d2ea5c8", + "prevId": "f7a9fd28-8bd2-4421-a048-a0a768fc8475", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 15f2e2e43bb..769b282f8b4 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1688,6 +1688,13 @@ "when": 1781700000000, "tag": "0241_drop_table_row_cap_guard", "breakpoints": true + }, + { + "idx": 242, + "version": "7", + "when": 1781818772450, + "tag": "0242_public_share", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 67a7b99f935..54366a6a024 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1389,6 +1389,39 @@ export const workspaceFiles = pgTable( }) ) +/** + * Public share links for workspace resources. Polymorphic on `resourceType` so a + * single mechanism serves files now and folders later. One row per resource + * (disable/re-enable flips `isActive` and keeps the same token). + */ +export const publicShare = pgTable( + 'public_share', + { + id: text('id').primaryKey(), + resourceType: text('resource_type').notNull(), // 'file' | 'folder' (folder reserved for future) + resourceId: text('resource_id').notNull(), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + // SET NULL (not CASCADE) so a share — and its public link — outlives the user + // who created it; the file still belongs to the workspace. + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), + token: text('token').notNull(), + isActive: boolean('is_active').notNull().default(true), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + tokenIdx: uniqueIndex('public_share_token_unique').on(table.token), + resourceUniqueIdx: uniqueIndex('public_share_resource_unique').on( + table.resourceType, + table.resourceId + ), + resourceIdIdx: index('public_share_resource_id_idx').on(table.resourceId), + workspaceIdIdx: index('public_share_workspace_id_idx').on(table.workspaceId), + }) +) + export const permissionTypeEnum = pgEnum('permission_type', ['admin', 'write', 'read']) export const invitationWorkspaceGrant = pgTable( diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 83e6d90d710..3fd8d55b04c 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -76,6 +76,8 @@ export const auditMock = { FILE_DELETED: 'file.deleted', FILE_RESTORED: 'file.restored', FILE_MOVED: 'file.moved', + FILE_SHARED: 'file.shared', + FILE_SHARE_DISABLED: 'file.share_disabled', FOLDER_CREATED: 'folder.created', FOLDER_UPDATED: 'folder.updated', FOLDER_DELETED: 'folder.deleted', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index bfd7a169d3c..8ee955cda1c 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 853, - zodRoutes: 853, + totalRoutes: 856, + zodRoutes: 856, nonZodRoutes: 0, } as const From c419a34317015329df77f34f5a8d2ab06e45d2ce Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 18 Jun 2026 19:05:19 -0700 Subject: [PATCH 07/16] feat(tables): raise per-plan table limits (free 5/50k, pro 100/100k, max 1k/500k) (#5135) Co-authored-by: Claude Opus 4.8 (1M context) --- .../app/(landing)/components/pricing/pricing.tsx | 8 ++++---- apps/sim/lib/core/config/env.ts | 12 ++++++------ apps/sim/lib/table/constants.ts | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/(landing)/components/pricing/pricing.tsx b/apps/sim/app/(landing)/components/pricing/pricing.tsx index d989f714167..b3d417ffba0 100644 --- a/apps/sim/app/(landing)/components/pricing/pricing.tsx +++ b/apps/sim/app/(landing)/components/pricing/pricing.tsx @@ -38,7 +38,7 @@ const PRICING_TIERS: PricingTier[] = [ features: [ '1,000 credits (trial)', '5GB file storage', - '3 tables · 1,000 rows each', + '5 tables · 50,000 rows each', '1 personal workspace', '5 min execution limit', '7-day log retention', @@ -56,7 +56,7 @@ const PRICING_TIERS: PricingTier[] = [ features: [ '6,000 credits/mo · +50/day', '50GB file storage', - '25 tables · 5,000 rows each', + '100 tables · 100,000 rows each', 'Up to 3 personal workspaces', '50 min execution · 150 runs/min', 'Unlimited log retention', @@ -74,7 +74,7 @@ const PRICING_TIERS: PricingTier[] = [ features: [ '25,000 credits/mo · +200/day', '500GB file storage', - '25 tables · 5,000 rows each', + '1,000 tables · 500,000 rows each', 'Up to 10 personal workspaces', '50 min execution · 300 runs/min', 'Unlimited log retention', @@ -91,7 +91,7 @@ const PRICING_TIERS: PricingTier[] = [ features: [ 'Custom credits & infra limits', 'Custom file storage', - '10,000 tables · 1M rows each', + 'Custom tables & rows', 'Unlimited shared workspaces', 'Custom execution limits', 'Unlimited log retention', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 5888e8d537f..27d810ed73b 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -75,12 +75,12 @@ export const env = createEnv({ TABLE_SNAPSHOT_CACHE: z.boolean().optional(), // Mount tables into sandboxes by reference via a version-keyed CSV snapshot in object storage instead of draining the whole table into web-process heap // Table feature limits (per plan). Apply when billing is disabled (free tier defaults) or for billed plans. - FREE_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on free tier (default: 3) - FREE_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on free tier (default: 1000) - PRO_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on pro tier (default: 25) - PRO_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on pro tier (default: 5000) - TEAM_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on team tier (default: 100) - TEAM_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on team tier (default: 10000) + FREE_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on free tier (default: 5) + FREE_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on free tier (default: 50000) + PRO_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on pro tier (default: 100) + PRO_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on pro tier (default: 100000) + TEAM_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on team tier (default: 1000) + TEAM_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on team tier (default: 500000) ENTERPRISE_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on enterprise tier (default: 10000) ENTERPRISE_TABLE_ROWS_LIMIT: z.number().optional(), // Max rows per table on enterprise tier (default: 1000000) diff --git a/apps/sim/lib/table/constants.ts b/apps/sim/lib/table/constants.ts index 0a276f68a41..22a3bd9a595 100644 --- a/apps/sim/lib/table/constants.ts +++ b/apps/sim/lib/table/constants.ts @@ -31,7 +31,7 @@ export const TABLE_LIMITS = { * keyset-select and cancel/ownership-check granularity. */ DELETE_PAGE_SIZE: 10000, /** Row count above which an export runs as a background job instead of a synchronous stream. - * Matches the default per-table row cap, so non-enterprise tables keep instant downloads. */ + * Tables at or under this stream instantly; larger ones fall back to an async export job. */ EXPORT_ASYNC_THRESHOLD_ROWS: 10000, /** Cap on the exclusion set ("select all, minus these") sent to an async delete job. */ MAX_EXCLUDE_ROW_IDS: 10000, @@ -44,16 +44,16 @@ export const TABLE_LIMITS = { */ export const DEFAULT_TABLE_PLAN_LIMITS = { free: { - maxTables: 3, - maxRowsPerTable: 1000, + maxTables: 5, + maxRowsPerTable: 50000, }, pro: { - maxTables: 25, - maxRowsPerTable: 5000, + maxTables: 100, + maxRowsPerTable: 100000, }, team: { - maxTables: 100, - maxRowsPerTable: 10000, + maxTables: 1000, + maxRowsPerTable: 500000, }, enterprise: { maxTables: 10000, From 91f9dfdaec6da9ea0e6da104a1b23f2834807336 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 19 Jun 2026 12:47:09 -0700 Subject: [PATCH 08/16] improvement(governance): derived access (#5134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(governance): org-ws-credential roles clarity * revert isHosted * improvement(credentials): code cleanup * address comments * make kb cascade delete on user hard delete * revert env flags * chore(db): drop local 0242 migration to regenerate after merging staging Our 0242 collides with staging's 0242. Remove it (and its snapshot + journal entry) so the KB-cascade migration can be regenerated with the correct number on top of the merged staging migrations. * chore(db): regenerate kb→workspace cascade migration as 0243 Regenerated via drizzle-kit generate on top of the merged staging migrations (staging took 0242). Re-applied the safety edits: NOT VALID + separate VALIDATE on the FK re-add, and the -- migration-safe note on the DROP. check:migrations passes. * improve copy * update docs --- .claude/rules/sim-architecture.md | 2 +- .cursor/rules/sim-architecture.mdc | 2 +- .cursor/rules/sim-testing.mdc | 2 +- .github/CONTRIBUTING.md | 2 +- AGENTS.md | 4 +- CLAUDE.md | 4 +- .../content/docs/en/platform/permissions.mdx | 90 +- apps/realtime/package.json | 2 +- apps/realtime/src/database/operations.ts | 2 +- apps/realtime/src/handlers/operations.ts | 2 +- apps/realtime/src/handlers/subblocks.ts | 2 +- apps/realtime/src/handlers/variables.ts | 2 +- .../src/middleware/permissions.test.ts | 2 +- apps/realtime/src/middleware/permissions.ts | 2 +- apps/sim/AGENTS.md | 2 +- .../app/api/auth/oauth/credentials/route.ts | 75 +- apps/sim/app/api/chat/utils.ts | 2 +- apps/sim/app/api/copilot/chat/queries.ts | 2 +- apps/sim/app/api/copilot/chats/route.test.ts | 9 + apps/sim/app/api/copilot/chats/route.ts | 42 +- .../api/copilot/checkpoints/revert/route.ts | 2 +- apps/sim/app/api/copilot/checkpoints/route.ts | 2 +- .../app/api/credentials/[id]/members/route.ts | 111 +- apps/sim/app/api/credentials/[id]/route.ts | 61 +- apps/sim/app/api/credentials/draft/route.ts | 21 +- apps/sim/app/api/credentials/route.ts | 61 +- apps/sim/app/api/files/authorization.ts | 4 +- .../app/api/folders/[id]/duplicate/route.ts | 2 +- apps/sim/app/api/folders/[id]/route.ts | 2 +- apps/sim/app/api/folders/reorder/route.ts | 2 +- apps/sim/app/api/folders/route.test.ts | 2 +- apps/sim/app/api/folders/route.ts | 2 +- apps/sim/app/api/guardrails/validate/route.ts | 2 +- apps/sim/app/api/jobs/[jobId]/route.test.ts | 2 +- apps/sim/app/api/jobs/[jobId]/route.ts | 4 +- .../documents/[documentId]/chunks/route.ts | 2 +- .../app/api/knowledge/[id]/documents/route.ts | 2 +- .../knowledge/[id]/documents/upsert/route.ts | 2 +- apps/sim/app/api/knowledge/search/route.ts | 2 +- .../api/logs/execution/[executionId]/route.ts | 34 +- apps/sim/app/api/logs/export/route.ts | 23 +- apps/sim/app/api/logs/stats/route.ts | 35 +- apps/sim/app/api/logs/triggers/route.ts | 16 +- apps/sim/app/api/mcp/discover/route.ts | 22 +- .../organizations/[id]/invitations/route.ts | 5 +- .../[id]/members/[memberId]/route.ts | 7 +- .../[memberId]/usage-limit/route.test.ts | 22 + .../members/[memberId]/usage-limit/route.ts | 6 +- .../api/organizations/[id]/members/route.ts | 5 +- .../api/organizations/[id]/roster/route.ts | 30 +- apps/sim/app/api/organizations/[id]/route.ts | 5 +- apps/sim/app/api/organizations/route.ts | 3 +- apps/sim/app/api/schedules/[id]/route.ts | 2 +- apps/sim/app/api/schedules/route.ts | 2 +- apps/sim/app/api/table/utils.ts | 9 +- apps/sim/app/api/tools/custom/route.ts | 2 +- .../app/api/tools/deployments/deploy/route.ts | 2 +- .../api/tools/deployments/promote/route.ts | 2 +- .../app/api/tools/deployments/routes.test.ts | 3 +- .../api/tools/deployments/undeploy/route.ts | 2 +- apps/sim/app/api/tools/deployments/utils.ts | 2 +- .../v1/admin/workflows/[id]/deploy/route.ts | 2 +- .../app/api/v1/admin/workflows/[id]/route.ts | 2 +- .../versions/[versionId]/activate/route.ts | 2 +- .../v1/admin/workflows/[id]/versions/route.ts | 2 +- apps/sim/app/api/v1/logs/route.ts | 18 +- apps/sim/app/api/v1/middleware.ts | 8 +- .../v1/workflows/[id]/deploy/route.test.ts | 3 +- .../app/api/v1/workflows/[id]/deploy/route.ts | 2 +- .../v1/workflows/[id]/rollback/route.test.ts | 3 +- .../api/v1/workflows/[id]/rollback/route.ts | 2 +- apps/sim/app/api/v1/workflows/[id]/route.ts | 2 +- apps/sim/app/api/v1/workflows/utils.ts | 2 +- apps/sim/app/api/webhooks/[id]/route.ts | 2 +- apps/sim/app/api/webhooks/route.ts | 17 +- .../api/workflows/[id]/autolayout/route.ts | 4 +- .../api/workflows/[id]/chat/status/route.ts | 2 +- .../app/api/workflows/[id]/deploy/route.ts | 2 +- .../deployments/[version]/revert/route.ts | 2 +- .../app/api/workflows/[id]/duplicate/route.ts | 2 +- .../app/api/workflows/[id]/execute/route.ts | 2 +- .../executions/[executionId]/cancel/route.ts | 2 +- .../[executionId]/stream/route.test.ts | 2 +- .../executions/[executionId]/stream/route.ts | 2 +- .../app/api/workflows/[id]/restore/route.ts | 6 +- apps/sim/app/api/workflows/[id]/route.ts | 2 +- .../sim/app/api/workflows/[id]/state/route.ts | 4 +- .../app/api/workflows/[id]/variables/route.ts | 4 +- apps/sim/app/api/workflows/middleware.test.ts | 2 +- apps/sim/app/api/workflows/middleware.ts | 2 +- apps/sim/app/api/workflows/reorder/route.ts | 2 +- apps/sim/app/api/workflows/route.test.ts | 2 +- apps/sim/app/api/workflows/route.ts | 2 +- .../api/workspaces/[id]/environment/route.ts | 17 +- .../[id]/metrics/executions/route.ts | 19 +- .../api/workspaces/[id]/permissions/route.ts | 31 +- apps/sim/app/api/workspaces/[id]/route.ts | 32 +- .../api/workspaces/invitations/route.test.ts | 18 +- .../app/api/workspaces/invitations/route.ts | 24 +- .../app/api/workspaces/members/[id]/route.ts | 14 +- apps/sim/app/api/workspaces/route.ts | 27 +- .../components/credential-members-section.tsx | 36 +- .../settings/components/billing/billing.tsx | 3 +- .../credential-sets/credential-sets.tsx | 3 +- .../manage-credits-modal.tsx | 5 +- .../organization-member-lists.tsx | 50 +- .../components/teammates/teammates.tsx | 39 +- .../upgrade/hooks/use-upgrade-state.ts | 3 +- apps/sim/components/permissions/index.ts | 7 + apps/sim/components/permissions/role-lock.tsx | 54 + .../components/access-control.tsx | 4 +- .../components/data-drains-settings.tsx | 3 +- .../components/data-retention-settings.tsx | 3 +- .../components/whitelabeling-settings.tsx | 3 +- .../executor/handlers/agent/agent-handler.ts | 1 + .../evaluator/evaluator-handler.test.ts | 16 + .../handlers/evaluator/evaluator-handler.ts | 1 + .../handlers/router/router-handler.test.ts | 17 + .../handlers/router/router-handler.ts | 12 +- apps/sim/executor/utils/vertex-credential.ts | 42 +- apps/sim/hooks/queries/utils/folder-tree.ts | 4 +- apps/sim/lib/api/contracts/credentials.ts | 2 + apps/sim/lib/api/contracts/organization.ts | 2 + apps/sim/lib/api/contracts/workspaces.ts | 1 + apps/sim/lib/auth/auth.ts | 16 +- apps/sim/lib/auth/credential-access.ts | 31 +- apps/sim/lib/billing/client/upgrade.ts | 5 +- apps/sim/lib/billing/core/billing.ts | 9 +- apps/sim/lib/billing/core/organization.ts | 20 +- apps/sim/lib/billing/core/subscription.ts | 18 +- apps/sim/lib/billing/core/usage.ts | 3 +- apps/sim/lib/billing/organization.ts | 3 +- .../lib/billing/organizations/membership.ts | 118 +- apps/sim/lib/billing/webhooks/invoices.ts | 9 +- apps/sim/lib/copilot/auth/permissions.test.ts | 155 +- apps/sim/lib/copilot/auth/permissions.ts | 41 +- apps/sim/lib/copilot/chat/lifecycle.test.ts | 2 +- apps/sim/lib/copilot/chat/lifecycle.ts | 2 +- apps/sim/lib/copilot/chat/post.ts | 50 +- apps/sim/lib/copilot/chat/process-contents.ts | 2 +- apps/sim/lib/copilot/tools/handlers/access.ts | 34 +- .../tools/handlers/workflow/mutations.ts | 2 +- .../tools/server/user/get-credentials.ts | 39 +- .../server/workflow/edit-workflow/index.ts | 5 +- .../validation/selector-validator.test.ts | 23 + .../copilot/validation/selector-validator.ts | 52 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 8 +- apps/sim/lib/credentials/access.test.ts | 118 + apps/sim/lib/credentials/access.ts | 163 +- apps/sim/lib/credentials/environment.ts | 91 +- apps/sim/lib/environment/utils.ts | 15 +- apps/sim/lib/execution/preprocessing.test.ts | 2 +- apps/sim/lib/execution/preprocessing.ts | 2 +- .../preprocessing.webhook-correlation.test.ts | 2 +- apps/sim/lib/invitations/core.ts | 8 +- .../lib/invitations/workspace-invitations.ts | 17 +- apps/sim/lib/logs/fetch-log-detail.ts | 21 +- apps/sim/lib/logs/list-logs.test.ts | 11 + apps/sim/lib/logs/list-logs.ts | 19 +- apps/sim/lib/mcp/middleware.ts | 12 +- apps/sim/lib/mothership/inbox/executor.ts | 27 +- .../sim/lib/workflows/orchestration/deploy.ts | 2 +- .../orchestration/workflow-lifecycle.ts | 2 +- .../lib/workflows/persistence/duplicate.ts | 5 +- apps/sim/lib/workflows/persistence/utils.ts | 2 +- apps/sim/lib/workflows/queries.ts | 10 +- apps/sim/lib/workflows/utils.test.ts | 2 +- apps/sim/lib/workflows/utils.ts | 13 +- apps/sim/lib/workspace-events/emitter.test.ts | 2 +- apps/sim/lib/workspace-events/emitter.ts | 2 +- apps/sim/lib/workspaces/organization/utils.ts | 3 +- .../lib/workspaces/permissions/utils.test.ts | 458 +- apps/sim/lib/workspaces/permissions/utils.ts | 268 +- apps/sim/lib/workspaces/policy.ts | 5 +- apps/sim/lib/workspaces/utils.test.ts | 64 +- apps/sim/lib/workspaces/utils.ts | 182 +- apps/sim/package.json | 2 +- apps/sim/vitest.setup.ts | 2 +- bun.lock | 33 +- .../migrations/0243_kb_workspace_cascade.sql | 6 + .../db/migrations/meta/0243_snapshot.json | 16729 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 23 +- .../package.json | 16 +- packages/platform-authz/src/predicates.ts | 37 + .../src/workflow.ts} | 58 +- packages/platform-authz/src/workspace.ts | 55 + .../tsconfig.json | 0 packages/testing/src/mocks/index.ts | 2 +- .../testing/src/mocks/permissions.mock.ts | 2 - packages/testing/src/mocks/schema.mock.ts | 4 +- .../testing/src/mocks/workflow-authz.mock.ts | 12 +- .../testing/src/mocks/workflows-utils.mock.ts | 2 +- 193 files changed, 18988 insertions(+), 1562 deletions(-) create mode 100644 apps/sim/components/permissions/role-lock.tsx create mode 100644 apps/sim/lib/credentials/access.test.ts create mode 100644 packages/db/migrations/0243_kb_workspace_cascade.sql create mode 100644 packages/db/migrations/meta/0243_snapshot.json rename packages/{workflow-authz => platform-authz}/package.json (63%) create mode 100644 packages/platform-authz/src/predicates.ts rename packages/{workflow-authz/src/index.ts => platform-authz/src/workflow.ts} (86%) create mode 100644 packages/platform-authz/src/workspace.ts rename packages/{workflow-authz => platform-authz}/tsconfig.json (100%) diff --git a/.claude/rules/sim-architecture.md b/.claude/rules/sim-architecture.md index 9b6b37ecef9..bc52fd37001 100644 --- a/.claude/rules/sim-architecture.md +++ b/.claude/rules/sim-architecture.md @@ -29,7 +29,7 @@ apps/ └── realtime/ # Bun Socket.IO server (collaborative canvas) packages/ # @sim/* — audit, auth, db, logger, realtime-protocol, - # security, tsconfig, utils, workflow-authz, + # security, tsconfig, utils, platform-authz, # workflow-persistence, workflow-types ``` diff --git a/.cursor/rules/sim-architecture.mdc b/.cursor/rules/sim-architecture.mdc index 08c3df6bf5b..90bac74294d 100644 --- a/.cursor/rules/sim-architecture.mdc +++ b/.cursor/rules/sim-architecture.mdc @@ -28,7 +28,7 @@ apps/ └── realtime/ # Bun Socket.IO server (collaborative canvas) packages/ # @sim/* — audit, auth, db, logger, realtime-protocol, - # security, tsconfig, utils, workflow-authz, + # security, tsconfig, utils, platform-authz, # workflow-persistence, workflow-types ``` diff --git a/.cursor/rules/sim-testing.mdc b/.cursor/rules/sim-testing.mdc index ca3ceb1e946..515784d541b 100644 --- a/.cursor/rules/sim-testing.mdc +++ b/.cursor/rules/sim-testing.mdc @@ -22,7 +22,7 @@ These modules are mocked globally — do NOT re-mock them in test files unless y - `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store` - `@/blocks/registry` - `@trigger.dev/sdk` -- `@sim/workflow-authz` → `workflowAuthzMock` +- `@sim/platform-authz/workflow` → `workflowAuthzMock` ## Structure diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f4e3df0d31c..d1e18087f88 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -8,7 +8,7 @@ Thank you for your interest in contributing to Sim! Our goal is to provide devel > - `apps/sim/` — the main Next.js application (App Router, ReactFlow, Zustand, Shadcn, Tailwind CSS). > - `apps/realtime/` — a small Bun + Socket.IO server that powers the collaborative canvas. Shares DB and Better Auth secrets with `apps/sim` via `@sim/*` packages. > - `apps/docs/` — Fumadocs-based documentation site. -> - `packages/` — shared workspace packages (`@sim/db`, `@sim/auth`, `@sim/audit`, `@sim/workflow-types`, `@sim/workflow-persistence`, `@sim/workflow-authz`, `@sim/realtime-protocol`, `@sim/security`, `@sim/logger`, `@sim/utils`, `@sim/testing`, `@sim/tsconfig`). +> - `packages/` — shared workspace packages (`@sim/db`, `@sim/auth`, `@sim/audit`, `@sim/workflow-types`, `@sim/workflow-persistence`, `@sim/platform-authz`, `@sim/realtime-protocol`, `@sim/security`, `@sim/logger`, `@sim/utils`, `@sim/testing`, `@sim/tsconfig`). > > Strict one-way dependency flow: `apps/* → packages/*`. Packages never import from apps. Please ensure your contributions follow this and our best practices for clarity, maintainability, and consistency. diff --git a/AGENTS.md b/AGENTS.md index 78feaedb30a..9ce16b909d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,11 +51,11 @@ packages/ ├── auth/ # @sim/auth — shared Better Auth verifier ├── db/ # @sim/db — drizzle schema + client ├── logger/ # @sim/logger +├── platform-authz/ # @sim/platform-authz — workspace + workflow authz (subpath exports) ├── realtime-protocol/ # @sim/realtime-protocol — socket op constants + zod schemas ├── security/ # @sim/security — safeCompare ├── tsconfig/ # shared tsconfig presets ├── utils/ # @sim/utils -├── workflow-authz/ # @sim/workflow-authz ├── workflow-persistence/ # @sim/workflow-persistence └── workflow-types/ # @sim/workflow-types — pure BlockState/Loop/Parallel types ``` @@ -409,7 +409,7 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/s ### Global Mocks (vitest.setup.ts) -`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/workflow-authz`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.) +`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/platform-authz/workflow`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.) ### Standard Test Pattern diff --git a/CLAUDE.md b/CLAUDE.md index 78feaedb30a..9ce16b909d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,11 +51,11 @@ packages/ ├── auth/ # @sim/auth — shared Better Auth verifier ├── db/ # @sim/db — drizzle schema + client ├── logger/ # @sim/logger +├── platform-authz/ # @sim/platform-authz — workspace + workflow authz (subpath exports) ├── realtime-protocol/ # @sim/realtime-protocol — socket op constants + zod schemas ├── security/ # @sim/security — safeCompare ├── tsconfig/ # shared tsconfig presets ├── utils/ # @sim/utils -├── workflow-authz/ # @sim/workflow-authz ├── workflow-persistence/ # @sim/workflow-persistence └── workflow-types/ # @sim/workflow-types — pure BlockState/Loop/Parallel types ``` @@ -409,7 +409,7 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/s ### Global Mocks (vitest.setup.ts) -`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/workflow-authz`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.) +`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/platform-authz/workflow`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.) ### Standard Test Pattern diff --git a/apps/docs/content/docs/en/platform/permissions.mdx b/apps/docs/content/docs/en/platform/permissions.mdx index 7dc4980ace3..dd7f24b20ac 100644 --- a/apps/docs/content/docs/en/platform/permissions.mdx +++ b/apps/docs/content/docs/en/platform/permissions.mdx @@ -1,13 +1,36 @@ --- title: Roles and permissions -description: Organization roles, workspace permission levels, and who can do what. +description: How organization, workspace, and credential roles work together — and how they inherit. pageType: reference --- import { Callout } from 'fumadocs-ui/components/callout' import { Video } from '@/components/ui/video' -Access in Sim has two layers: **organization roles** (Owner, Admin, Member) govern the organization itself, and **workspace permissions** (Read, Write, Admin) govern what each member can do inside a workspace. +Access in Sim is organized into three nested levels — your **organization**, the **workspaces** inside it, and the **credentials** (connected accounts and secrets) inside those. Each level has its own roles, and roles **inherit downward**: an admin at one level is automatically an admin at the level below. Inheritance only ever *adds* access — it never takes access away from someone who already has it. + +## How roles inherit + +Each level has its own set of roles: + +| Level | Roles | +|-------|-------| +| **Organization** | Owner, Admin, Member | +| **Workspace** | Read, Write, Admin | +| **Credentials** (shared connections and secrets) | Member, Admin | + +Higher roles flow down automatically: + +- An organization **Owner or Admin** is automatically an **Admin of every workspace** in the organization — no per-workspace invite required. +- A workspace **Admin** is automatically an **Admin of every shared credential** in that workspace — OAuth connections, service accounts, and workspace secrets. + +Put together, an organization Owner or Admin can administer every workspace and every shared credential in the organization, top to bottom. + +Inherited roles are **automatic and locked**. In member lists they show greyed out with a short tooltip saying where the role comes from (for example, *"Organization admins are automatically workspace admins"*), and they can't be lowered there — you change them at the level they come from. + + +**Personal secrets are the one exception.** A user's personal environment variables stay private to them and are never shared or inherited — not by workspace Admins, not by organization Owners or Admins, not by anyone. + ## Workspaces and Organizations @@ -96,32 +119,20 @@ Here's a detailed breakdown of what users can do with each permission level: - Invite new users to the workspace with any permission level - Remove users from the workspace - Manage workspace settings and integrations -- Configure external tool connections +- Administer every shared credential in the workspace (OAuth connections, service accounts, and workspace secrets) - Delete workflows created by other users +- Delete the workspace **What they cannot do:** -- Delete the workspace (only the workspace owner can do this) -- Remove the workspace owner from the workspace +- Change a role that's inherited from a higher level — an organization admin's workspace role, or the owner's, is locked and managed where it comes from --- -## Workspace Owner vs Admin - -Every workspace has one **Owner** (the person who created it) plus any number of **Admins**. +## Workspace Owner -### Workspace Owner -- Has all Admin permissions -- Can delete the workspace -- Cannot be removed from the workspace -- Can transfer ownership to another user +Every workspace has one **Owner** — usually the person who created it. The Owner is simply an Admin whose role is fixed: in the member list it shows as a locked **Admin** (tooltip *"Workspace owner"*), so it can't be lowered. An Owner has no abilities a regular Admin lacks. -### Workspace Admin -- Can do everything except delete the workspace or remove the owner -- Can be removed from the workspace by the owner or other admins - - - For shared (organization) workspaces, the organization's Owner and Admins are treated as Admins of every workspace in the organization, even without an explicit per-workspace invite. - +Any Admin — whether invited directly or an admin by way of their organization role — can manage members and settings and delete the workspace. On a shared workspace an Admin can also remove the Owner; ownership then passes to the organization's owner, so the workspace always has one. (The organization's owner is the one account that can't be removed this way — they're the final fallback.) On your personal workspace you are the Owner and can't be removed. --- @@ -146,15 +157,35 @@ Every workspace has one **Owner** (the person who created it) plus any number of Users can create two types of environment variables: ### Personal Environment Variables -- Only visible to the individual user +- Only visible to the individual user, and never shared or inherited — not even by workspace or organization admins - Available in all workflows they run - Managed in **Settings**, then go to **Secrets** ### Workspace Environment Variables -- **Read permission**: Can see variable names and values -- **Write/Admin permission**: Can add, edit, and delete variables -- Available to all workspace members -- If a personal variable has the same name as a workspace variable, the personal one takes priority +- **Read**: see variable names (the values stay hidden unless you're an admin of that secret) +- **Write**: add new variables, and edit or delete ones you created +- **Admin**: add, edit, delete, and view the values of any workspace variable +- Workspace variables are a kind of workspace credential, so they follow the [Credential Access](#credential-access) rules below — workspace Admins are admins of all of them +- Available to all workspace members. If a workspace variable and a personal variable share the same name, the **workspace** value wins when a workflow runs + +--- + +## Credential Access + +Workspace credentials — OAuth connections, service accounts, and workspace environment variables — have two roles of their own: + +- **Credential Member**: can use the credential in workflows. +- **Credential Admin**: can use it and also edit, delete, and share it. + +These roles follow your workspace role: + +- **Workspace Admins are automatically Credential Admins** of every shared credential in the workspace (OAuth connections, service accounts, and workspace environment variables). Because organization Owners and Admins are workspace Admins everywhere, they are Credential Admins too. These automatic roles are fixed — they show greyed out with a tooltip in the credential's member list and cannot be changed. +- **Read and Write members are Credential Members** by default — they can use shared credentials but cannot edit, delete, or share them unless someone makes them a Credential Admin (you are always an admin of credentials you create). +- **Personal environment variables** are the exception: they stay private to their owner and are never shared with workspace admins. + + + A Credential Admin can both use and manage a credential, so a workspace Admin can run workflows that use any shared OAuth connection in the workspace — including one another member added. + --- @@ -173,7 +204,7 @@ An organization has three roles: **Owner**, **Admin**, and **Member**. - Invite and remove team members from the organization - Create new shared workspaces under the organization - Manage billing, seat count, and subscription settings -- Access all shared workspaces within the organization as a workspace Admin +- Access every shared workspace in the organization as a workspace Admin automatically (no per-workspace invite), including administering the credentials inside them - Promote members to Admin or demote Admins to Member @@ -192,8 +223,9 @@ import { FAQ } from '@/components/ui/faq' { question: "What is the difference between organization roles and workspace permissions?", answer: "Organization roles (Owner, Admin, or Member) control who can manage the organization itself, including inviting people, creating shared workspaces, and handling billing. Workspace permissions (Read, Write, Admin) control what a user can do within a specific workspace, such as viewing, editing, or managing workflows. Internal members need both an organization role and a workspace permission to work within a shared workspace. External workspace members do not have an organization role in your org; they only have workspace-level access." }, { question: "What happens to my shared workspaces if I cancel or downgrade my Team plan?", answer: "Existing shared workspaces remain accessible to current members, but new invitations are disabled until you upgrade back to a Team or Enterprise plan. No workspaces or members are deleted — the organization is simply dormant until billing is re-enabled." }, { question: "Can I restrict which integrations or model providers a team member can use?", answer: "Yes, on Enterprise-entitled organizations. Any organization owner or admin can create permission groups with fine-grained controls, including restricting allowed integrations and allowed model providers to specific lists. You can also disable access to MCP tools, custom tools, skills, and various platform features like the knowledge base, API keys, or Copilot on a per-group basis. Permission groups are scoped to the organization and can govern either all workspaces or a specific subset — a user can belong to multiple groups but is governed by exactly one group in any given workspace." }, - { question: "What happens when a personal environment variable has the same name as a workspace variable?", answer: "The personal environment variable takes priority. When a workflow runs, if both a personal and workspace variable share the same name, the personal value is used. This allows individual users to override shared workspace configuration when needed." }, - { question: "Can an Admin remove the workspace owner?", answer: "No. The workspace owner cannot be removed from the workspace by anyone. Only the workspace owner can delete the workspace or transfer ownership to another user. Admins can do everything else, including inviting and removing other users and managing workspace settings." }, - { question: "What are permission groups and how do they work?", answer: "Permission groups are an Enterprise access control feature that lets organization owners and admins define granular restrictions beyond the standard Read/Write/Admin roles. Groups are scoped to the organization and can govern either all workspaces or a specific subset. A user can belong to multiple groups, but at most one governs them in any given workspace: a workspace-specific group takes precedence over an all-workspaces group, which takes precedence over the organization's default group. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers its members can access. Members are assigned manually, and an organization can designate one group as the default (always all-workspaces) that governs everyone not explicitly assigned — including external workspace members. Execution-time enforcement is based on the organization that owns the workflow's workspace, not the user's current UI context." }, + { question: "What happens when a personal environment variable has the same name as a workspace variable?", answer: "The workspace variable wins. When a workflow runs, the resolver checks workspace variables first and falls back to a personal variable only when no workspace variable shares that name. This keeps shared, team-managed values authoritative in production workflows." }, + { question: "Can an Admin remove the workspace owner?", answer: "On a shared (organization) workspace, yes — any Admin can remove the workspace Owner, and ownership passes to the organization's owner so the workspace always has one. The organization's owner is the single account that can't be removed this way, since they're the final fallback. On your personal workspace you are the Owner and can't remove yourself. The Owner is not a higher permission tier than Admin: every Admin — including those who inherit the role from their organization — can manage members and settings and delete the workspace." }, + { question: "Who can manage a workspace's credentials and secrets?", answer: "Workspace Admins are automatically Credential Admins of the workspace's shared credentials — OAuth connections, service accounts, and workspace environment variables — so they can use, edit, delete, and share them, and run workflows that rely on them. Organization Owners and Admins get this too because they are workspace Admins everywhere. Read and Write members get use-only access to shared credentials unless they are explicitly made a Credential Admin. Personal environment variables are never shared; they stay private to their owner." }, + { question: "What are permission groups and how do they work?", answer: "Permission groups are an Enterprise access control feature that lets organization owners and admins define granular restrictions beyond the standard Read/Write/Admin roles. Groups are scoped to the organization and can govern either all workspaces or a specific subset. A user can belong to multiple groups, but at most one governs them in any given workspace: a workspace-specific group takes precedence over an all-workspaces group, which takes precedence over the organization's default group. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers its members can access. Members are assigned manually, and an organization can designate one group as the default (always all-workspaces) that governs everyone not explicitly assigned — including external workspace members. Restrictions are enforced based on the organization that owns the workflow's workspace, not on which workspace you're currently viewing." }, { question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite teammates to the organization as Members, then add them to the relevant workspace with Read permission if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users. For clients, partners, or users who already belong to another Sim organization, use external workspace access so they can collaborate without joining your organization or consuming a seat." }, ]} /> \ No newline at end of file diff --git a/apps/realtime/package.json b/apps/realtime/package.json index 17f412773e1..99867ef852d 100644 --- a/apps/realtime/package.json +++ b/apps/realtime/package.json @@ -24,10 +24,10 @@ "@sim/auth": "workspace:*", "@sim/db": "workspace:*", "@sim/logger": "workspace:*", + "@sim/platform-authz": "workspace:*", "@sim/realtime-protocol": "workspace:*", "@sim/security": "workspace:*", "@sim/utils": "workspace:*", - "@sim/workflow-authz": "workspace:*", "@sim/workflow-persistence": "workspace:*", "@sim/workflow-types": "workspace:*", "@socket.io/redis-adapter": "8.3.0", diff --git a/apps/realtime/src/database/operations.ts b/apps/realtime/src/database/operations.ts index c5474a5f6f8..ac0072c959d 100644 --- a/apps/realtime/src/database/operations.ts +++ b/apps/realtime/src/database/operations.ts @@ -8,6 +8,7 @@ import { workflowSubflows, } from '@sim/db' import { createLogger } from '@sim/logger' +import { getActiveWorkflowContext } from '@sim/platform-authz/workflow' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -20,7 +21,6 @@ import { WORKFLOW_OPERATIONS, } from '@sim/realtime-protocol/constants' import { randomFloat } from '@sim/utils/random' -import { getActiveWorkflowContext } from '@sim/workflow-authz' import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load' import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks' import { isWorkflowBlockProtected } from '@sim/workflow-types/workflow' diff --git a/apps/realtime/src/handlers/operations.ts b/apps/realtime/src/handlers/operations.ts index 49d5bbcd0b2..eef51847718 100644 --- a/apps/realtime/src/handlers/operations.ts +++ b/apps/realtime/src/handlers/operations.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -11,7 +12,6 @@ import { import { WorkflowOperationSchema } from '@sim/realtime-protocol/schemas' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' import { ZodError } from 'zod' import { persistWorkflowOperation } from '@/database/operations' import type { AuthenticatedSocket } from '@/middleware/auth' diff --git a/apps/realtime/src/handlers/subblocks.ts b/apps/realtime/src/handlers/subblocks.ts index 0295aff458c..b2f94b6fb98 100644 --- a/apps/realtime/src/handlers/subblocks.ts +++ b/apps/realtime/src/handlers/subblocks.ts @@ -1,9 +1,9 @@ import { db } from '@sim/db' import { workflow, workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants' import { getErrorMessage } from '@sim/utils/errors' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' import { isWorkflowBlockProtected } from '@sim/workflow-types/workflow' import { and, eq } from 'drizzle-orm' import type { AuthenticatedSocket } from '@/middleware/auth' diff --git a/apps/realtime/src/handlers/variables.ts b/apps/realtime/src/handlers/variables.ts index f9b1c1f0c68..98dc3a5b7af 100644 --- a/apps/realtime/src/handlers/variables.ts +++ b/apps/realtime/src/handlers/variables.ts @@ -1,9 +1,9 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { VARIABLE_OPERATIONS } from '@sim/realtime-protocol/constants' import { getErrorMessage } from '@sim/utils/errors' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import type { AuthenticatedSocket } from '@/middleware/auth' import { checkWorkflowOperationPermission } from '@/middleware/permissions' diff --git a/apps/realtime/src/middleware/permissions.test.ts b/apps/realtime/src/middleware/permissions.test.ts index 554ba8355fd..c1078b8c0c0 100644 --- a/apps/realtime/src/middleware/permissions.test.ts +++ b/apps/realtime/src/middleware/permissions.test.ts @@ -19,7 +19,7 @@ const { mockAuthorize } = vi.hoisted(() => ({ mockAuthorize: vi.fn(), })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorize, })) diff --git a/apps/realtime/src/middleware/permissions.ts b/apps/realtime/src/middleware/permissions.ts index 23069ff51de..00fc5c9580f 100644 --- a/apps/realtime/src/middleware/permissions.ts +++ b/apps/realtime/src/middleware/permissions.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -11,7 +12,6 @@ import { VARIABLE_OPERATIONS, WORKFLOW_OPERATIONS, } from '@sim/realtime-protocol/constants' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' const logger = createLogger('SocketPermissions') diff --git a/apps/sim/AGENTS.md b/apps/sim/AGENTS.md index a766fb697d5..6c52c2df02d 100644 --- a/apps/sim/AGENTS.md +++ b/apps/sim/AGENTS.md @@ -29,7 +29,7 @@ apps/ └── realtime/ # Bun Socket.IO server (collaborative canvas) packages/ # @sim/* — audit, auth, db, logger, realtime-protocol, - # security, tsconfig, utils, workflow-authz, + # security, tsconfig, utils, platform-authz, # workflow-persistence, workflow-types ``` diff --git a/apps/sim/app/api/auth/oauth/credentials/route.ts b/apps/sim/app/api/auth/oauth/credentials/route.ts index cb81a810b1b..a3ff4c12097 100644 --- a/apps/sim/app/api/auth/oauth/credentials/route.ts +++ b/apps/sim/app/api/auth/oauth/credentials/route.ts @@ -1,14 +1,15 @@ import { db } from '@sim/db' import { account, credential, credentialMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' -import { and, eq } from 'drizzle-orm' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' +import { and, eq, isNotNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { oauthCredentialsQuerySchema } from '@/lib/api/contracts/credentials' import { getValidationErrorMessage } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getCredentialActorContext } from '@/lib/credentials/access' import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth' import { getCanonicalScopesForProvider, @@ -114,11 +115,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { effectiveWorkspaceId = workflowAuthorization.workflow?.workspaceId || undefined } + let requesterCanAdmin = false if (effectiveWorkspaceId) { const workspaceAccess = await checkWorkspaceAccess(effectiveWorkspaceId, requesterUserId) if (!workspaceAccess.hasAccess) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } + requesterCanAdmin = workspaceAccess.canAdmin } if (credentialId) { @@ -150,19 +153,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } if (!workflowId) { - const [membership] = await db - .select({ id: credentialMember.id }) - .from(credentialMember) - .where( - and( - eq(credentialMember.credentialId, platformCredential.id), - eq(credentialMember.userId, requesterUserId), - eq(credentialMember.status, 'active') - ) - ) - .limit(1) - - if (!membership) { + const access = await getCredentialActorContext(platformCredential.id, requesterUserId) + if (!access.hasWorkspaceAccess || (!access.member && !access.isAdmin)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } @@ -193,19 +185,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } else { - const [membership] = await db - .select({ id: credentialMember.id }) - .from(credentialMember) - .where( - and( - eq(credentialMember.credentialId, platformCredential.id), - eq(credentialMember.userId, requesterUserId), - eq(credentialMember.status, 'active') - ) - ) - .limit(1) - - if (!membership) { + const access = await getCredentialActorContext(platformCredential.id, requesterUserId) + if (!access.hasWorkspaceAccess || (!access.member && !access.isAdmin)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } @@ -237,17 +218,18 @@ export const GET = withRouteHandler(async (request: NextRequest) => { userId: requesterUserId, }) + const oauthSelect = { + id: credential.id, + displayName: credential.displayName, + providerId: account.providerId, + scope: account.scope, + updatedAt: account.updatedAt, + } const credentialsData = await db - .select({ - id: credential.id, - displayName: credential.displayName, - providerId: account.providerId, - scope: account.scope, - updatedAt: account.updatedAt, - }) + .select(oauthSelect) .from(credential) .innerJoin(account, eq(credential.accountId, account.id)) - .innerJoin( + .leftJoin( credentialMember, and( eq(credentialMember.credentialId, credential.id), @@ -259,7 +241,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { and( eq(credential.workspaceId, effectiveWorkspaceId), eq(credential.type, 'oauth'), - eq(account.providerId, providerParam) + eq(account.providerId, providerParam), + requesterCanAdmin ? undefined : isNotNull(credentialMember.id) ) ) @@ -270,15 +253,16 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const saProviderId = getServiceAccountProviderForProviderId(providerParam) if (saProviderId) { + const saSelect = { + id: credential.id, + displayName: credential.displayName, + providerId: credential.providerId, + updatedAt: credential.updatedAt, + } const serviceAccountCreds = await db - .select({ - id: credential.id, - displayName: credential.displayName, - providerId: credential.providerId, - updatedAt: credential.updatedAt, - }) + .select(saSelect) .from(credential) - .innerJoin( + .leftJoin( credentialMember, and( eq(credentialMember.credentialId, credential.id), @@ -290,7 +274,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { and( eq(credential.workspaceId, effectiveWorkspaceId), eq(credential.type, 'service_account'), - eq(credential.providerId, saProviderId) + eq(credential.providerId, saProviderId), + requesterCanAdmin ? undefined : isNotNull(credentialMember.id) ) ) diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 49c5f170645..680f96d8024 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' import { chat, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { safeCompare } from '@sim/security/compare' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest, NextResponse } from 'next/server' import { isWorkspaceApiExecutionEntitled } from '@/lib/billing/core/api-access' diff --git a/apps/sim/app/api/copilot/chat/queries.ts b/apps/sim/app/api/copilot/chat/queries.ts index 55d8f5acad0..c7fa4d58b3f 100644 --- a/apps/sim/app/api/copilot/chat/queries.ts +++ b/apps/sim/app/api/copilot/chat/queries.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { toError } from '@sim/utils/errors' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository' diff --git a/apps/sim/app/api/copilot/chats/route.test.ts b/apps/sim/app/api/copilot/chats/route.test.ts index 11046b7a349..2ce59e2a41d 100644 --- a/apps/sim/app/api/copilot/chats/route.test.ts +++ b/apps/sim/app/api/copilot/chats/route.test.ts @@ -24,6 +24,7 @@ vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), or: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'or' })), + inArray: vi.fn((field: unknown, values: unknown) => ({ field, values, type: 'inArray' })), isNull: vi.fn((field: unknown) => ({ field, type: 'isNull' })), desc: vi.fn((field: unknown) => ({ field, type: 'desc' })), sql: vi.fn(), @@ -31,6 +32,14 @@ vi.mock('drizzle-orm', () => ({ vi.mock('@/lib/copilot/request/http', () => copilotHttpMock) +vi.mock('@/lib/workspaces/utils', () => ({ + listAccessibleWorkspaceRowsForUser: vi + .fn() + .mockResolvedValue([ + { workspace: { id: 'workspace-123', createdAt: new Date() }, permissionType: 'admin' }, + ]), +})) + import { GET } from '@/app/api/copilot/chats/route' describe('Copilot Chats List API Route', () => { diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts index c72bfa1c8ba..5f72001816b 100644 --- a/apps/sim/app/api/copilot/chats/route.ts +++ b/apps/sim/app/api/copilot/chats/route.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' -import { copilotChats, permissions, workflow, workspace } from '@sim/db/schema' +import { copilotChats, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' -import { and, desc, eq, isNull, or, sql } from 'drizzle-orm' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' +import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createWorkflowCopilotChatContract } from '@/lib/api/contracts/copilot' import { parseRequest, validationErrorResponse } from '@/lib/api/server' @@ -20,6 +20,7 @@ import { assertActiveWorkspaceAccess, isWorkspaceAccessDeniedError, } from '@/lib/workspaces/permissions/utils' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' const logger = createLogger('CopilotChatsListAPI') @@ -32,6 +33,21 @@ export const GET = withRouteHandler(async (_request: NextRequest) => { return createUnauthorizedResponse() } + // Active accessible workspaces (explicit + org-derived). Using the active + // scope keeps the archived-workspace exclusion the old join-based query had. + const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId) + const accessibleWorkspaceIds = accessibleRows.map((row) => row.workspace.id) + const inAccessibleWorkspace = + accessibleWorkspaceIds.length > 0 + ? or( + inArray(workflow.workspaceId, accessibleWorkspaceIds), + and( + isNull(copilotChats.workflowId), + inArray(copilotChats.workspaceId, accessibleWorkspaceIds) + ) + ) + : undefined + const visibleChats = await db .selectDistinctOn([copilotChats.id], { id: copilotChats.id, @@ -43,30 +59,14 @@ export const GET = withRouteHandler(async (_request: NextRequest) => { }) .from(copilotChats) .leftJoin(workflow, eq(copilotChats.workflowId, workflow.id)) - .leftJoin( - workspace, - or( - eq(workflow.workspaceId, workspace.id), - and(isNull(copilotChats.workflowId), eq(copilotChats.workspaceId, workspace.id)) - ) - ) - .leftJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspace.id), - eq(permissions.userId, userId) - ) - ) .where( and( eq(copilotChats.userId, userId), or( and(isNull(copilotChats.workflowId), isNull(copilotChats.workspaceId)), - sql`${permissions.id} IS NOT NULL` + inAccessibleWorkspace ), - or(isNull(workflow.id), isNull(workflow.archivedAt)), - or(isNull(workspace.id), isNull(workspace.archivedAt)) + or(isNull(workflow.id), isNull(workflow.archivedAt)) ) ) .orderBy(copilotChats.id, desc(copilotChats.updatedAt)) diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.ts index 5ccda51212d..f784dc48d84 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflowCheckpoints, workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { revertCopilotCheckpointContract } from '@/lib/api/contracts/copilot' diff --git a/apps/sim/app/api/copilot/checkpoints/route.ts b/apps/sim/app/api/copilot/checkpoints/route.ts index 985a95a71a3..4bd861ffb50 100644 --- a/apps/sim/app/api/copilot/checkpoints/route.ts +++ b/apps/sim/app/api/copilot/checkpoints/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflowCheckpoints } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { diff --git a/apps/sim/app/api/credentials/[id]/members/route.ts b/apps/sim/app/api/credentials/[id]/members/route.ts index 7753d2fc21c..72132ee56d0 100644 --- a/apps/sim/app/api/credentials/[id]/members/route.ts +++ b/apps/sim/app/api/credentials/[id]/members/route.ts @@ -5,12 +5,19 @@ import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { upsertWorkspaceCredentialMemberContract } from '@/lib/api/contracts/credentials' +import { + upsertWorkspaceCredentialMemberContract, + type WorkspaceCredentialMember, +} from '@/lib/api/contracts/credentials' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { deriveCredentialAdmin, isSharedCredentialType } from '@/lib/credentials/access' import { captureServerEvent } from '@/lib/posthog/server' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { + getUserEntityPermissions, + getUsersWithPermissions, +} from '@/lib/workspaces/permissions/utils' const logger = createLogger('CredentialMembersAPI') @@ -18,7 +25,7 @@ interface RouteContext { params: Promise<{ id: string }> } -async function requireWorkspaceAdminMembership(credentialId: string, userId: string) { +async function requireCredentialAdmin(credentialId: string, userId: string) { const [cred] = await db .select({ id: credential.id, workspaceId: credential.workspaceId, type: credential.type }) .from(credential) @@ -38,10 +45,16 @@ async function requireWorkspaceAdminMembership(credentialId: string, userId: str ) .limit(1) - if (!membership || membership.status !== 'active' || membership.role !== 'admin') { + const isAdmin = deriveCredentialAdmin({ + credentialType: cred.type, + memberRole: membership?.status === 'active' ? membership.role : null, + workspaceCanAdmin: perm === 'admin', + }) + + if (!isAdmin) { return null } - return { ...membership, credentialType: cred.type, workspaceId: cred.workspaceId } + return { credentialType: cred.type, workspaceId: cred.workspaceId } } export const GET = withRouteHandler(async (_request: NextRequest, context: RouteContext) => { @@ -54,7 +67,7 @@ export const GET = withRouteHandler(async (_request: NextRequest, context: Route const { id: credentialId } = await context.params const [cred] = await db - .select({ id: credential.id, workspaceId: credential.workspaceId }) + .select({ id: credential.id, workspaceId: credential.workspaceId, type: credential.type }) .from(credential) .where(eq(credential.id, credentialId)) .limit(1) @@ -72,7 +85,7 @@ export const GET = withRouteHandler(async (_request: NextRequest, context: Route return NextResponse.json({ error: 'Not found' }, { status: 404 }) } - const members = await db + const explicitMembers = await db .select({ id: credentialMember.id, userId: credentialMember.userId, @@ -86,6 +99,48 @@ export const GET = withRouteHandler(async (_request: NextRequest, context: Route .innerJoin(user, eq(credentialMember.userId, user.id)) .where(eq(credentialMember.credentialId, credentialId)) + const byUser = new Map( + explicitMembers.map((m) => [ + m.userId, + { + id: m.id, + userId: m.userId, + role: m.role, + status: m.status, + joinedAt: m.joinedAt ? m.joinedAt.toISOString() : null, + userName: m.userName, + userEmail: m.userEmail, + roleSource: 'explicit' as const, + }, + ]) + ) + + if (isSharedCredentialType(cred.type)) { + const workspaceMembers = await getUsersWithPermissions(cred.workspaceId) + for (const wsMember of workspaceMembers) { + if (wsMember.permissionType !== 'admin') continue + const existing = byUser.get(wsMember.userId) + if (existing) { + existing.role = 'admin' + existing.status = 'active' + existing.roleSource = 'workspace-admin' + } else { + byUser.set(wsMember.userId, { + id: `workspace-admin-${wsMember.userId}`, + userId: wsMember.userId, + role: 'admin', + status: 'active', + joinedAt: null, + userName: wsMember.name, + userEmail: wsMember.email, + roleSource: 'workspace-admin', + }) + } + } + } + + const members = Array.from(byUser.values()) + return NextResponse.json({ members }) } catch (error) { logger.error('Failed to fetch credential members', { error }) @@ -102,7 +157,7 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route const { id: credentialId } = await context.params - const admin = await requireWorkspaceAdminMembership(credentialId, session.user.id) + const admin = await requireCredentialAdmin(credentialId, session.user.id) if (!admin) { logger.warn('Credential member share denied', { credentialId, @@ -111,7 +166,7 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route }) return NextResponse.json({ error: 'Admin access required' }, { status: 403 }) } - if (admin.credentialType === 'env_personal') { + if (!isSharedCredentialType(admin.credentialType)) { logger.warn('Credential member share denied', { credentialId, actorId: session.user.id, @@ -124,6 +179,19 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route if (!parsed.success) return parsed.response const { userId, role } = parsed.data.body + + const targetWorkspacePerm = await getUserEntityPermissions( + userId, + 'workspace', + admin.workspaceId + ) + if (targetWorkspacePerm === 'admin' && role !== 'admin') { + return NextResponse.json( + { error: 'Workspace admins are automatically credential admins and cannot be demoted' }, + { status: 400 } + ) + } + const now = new Date() const [existing] = await db @@ -142,7 +210,12 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route .where(eq(credentialMember.id, existing.id)) .limit(1) .for('update') - if (current?.role === 'admin' && current?.status === 'active' && role !== 'admin') { + if ( + !isSharedCredentialType(admin.credentialType) && + current?.role === 'admin' && + current?.status === 'active' && + role !== 'admin' + ) { const activeAdmins = await tx .select({ id: credentialMember.id }) .from(credentialMember) @@ -233,7 +306,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest, context: Rou return NextResponse.json({ error: 'userId query parameter required' }, { status: 400 }) } - const admin = await requireWorkspaceAdminMembership(credentialId, session.user.id) + const admin = await requireCredentialAdmin(credentialId, session.user.id) if (!admin) { logger.warn('Credential member removal denied', { credentialId, @@ -262,8 +335,22 @@ export const DELETE = withRouteHandler(async (request: NextRequest, context: Rou return NextResponse.json({ error: 'Member not found' }, { status: 404 }) } + if (isSharedCredentialType(admin.credentialType)) { + const targetWorkspacePerm = await getUserEntityPermissions( + targetUserId, + 'workspace', + admin.workspaceId + ) + if (targetWorkspacePerm === 'admin') { + return NextResponse.json( + { error: 'Workspace admins are automatically credential admins and cannot be removed' }, + { status: 400 } + ) + } + } + const revoked = await db.transaction(async (tx) => { - if (target.role === 'admin') { + if (!isSharedCredentialType(admin.credentialType) && target.role === 'admin') { const activeAdmins = await tx .select({ id: credentialMember.id }) .from(credentialMember) diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts index f846a88d3fb..3dc507ed5ae 100644 --- a/apps/sim/app/api/credentials/[id]/route.ts +++ b/apps/sim/app/api/credentials/[id]/route.ts @@ -1,44 +1,34 @@ -import { db } from '@sim/db' -import { credential, credentialMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateWorkspaceCredentialContract } from '@/lib/api/contracts/credentials' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getCredentialActorContext } from '@/lib/credentials/access' +import { type CredentialActorContext, getCredentialActorContext } from '@/lib/credentials/access' import { performDeleteCredential, performUpdateCredential } from '@/lib/credentials/orchestration' const logger = createLogger('CredentialByIdAPI') -async function getCredentialResponse(credentialId: string, userId: string) { - const [row] = await db - .select({ - id: credential.id, - workspaceId: credential.workspaceId, - type: credential.type, - displayName: credential.displayName, - description: credential.description, - providerId: credential.providerId, - accountId: credential.accountId, - envKey: credential.envKey, - envOwnerUserId: credential.envOwnerUserId, - createdBy: credential.createdBy, - createdAt: credential.createdAt, - updatedAt: credential.updatedAt, - role: credentialMember.role, - status: credentialMember.status, - }) - .from(credential) - .innerJoin( - credentialMember, - and(eq(credentialMember.credentialId, credential.id), eq(credentialMember.userId, userId)) - ) - .where(eq(credential.id, credentialId)) - .limit(1) - - return row ?? null +function formatCredentialResponse(access: CredentialActorContext) { + const cred = access.credential + if (!cred) return null + + return { + id: cred.id, + workspaceId: cred.workspaceId, + type: cred.type, + displayName: cred.displayName, + description: cred.description, + providerId: cred.providerId, + accountId: cred.accountId, + envKey: cred.envKey, + envOwnerUserId: cred.envOwnerUserId, + createdBy: cred.createdBy, + createdAt: cred.createdAt, + updatedAt: cred.updatedAt, + role: access.isAdmin ? 'admin' : (access.member?.role ?? null), + status: access.member?.status ?? (access.isAdmin ? 'active' : null), + } } export const GET = withRouteHandler( @@ -55,12 +45,11 @@ export const GET = withRouteHandler( if (!access.credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } - if (!access.hasWorkspaceAccess || !access.member) { + if (!access.hasWorkspaceAccess || (!access.member && !access.isAdmin)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - const row = await getCredentialResponse(id, session.user.id) - return NextResponse.json({ credential: row }, { status: 200 }) + return NextResponse.json({ credential: formatCredentialResponse(access) }, { status: 200 }) } catch (error) { logger.error('Failed to fetch credential', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) @@ -109,8 +98,8 @@ export const PUT = withRouteHandler( return NextResponse.json({ error: result.error }, { status }) } - const row = await getCredentialResponse(id, session.user.id) - return NextResponse.json({ credential: row }, { status: 200 }) + const access = await getCredentialActorContext(id, session.user.id) + return NextResponse.json({ credential: formatCredentialResponse(access) }, { status: 200 }) } catch (error) { logger.error('Failed to update credential', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) diff --git a/apps/sim/app/api/credentials/draft/route.ts b/apps/sim/app/api/credentials/draft/route.ts index 9efb27f2619..2e693609438 100644 --- a/apps/sim/app/api/credentials/draft/route.ts +++ b/apps/sim/app/api/credentials/draft/route.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { credential, credentialMember, pendingCredentialDraft } from '@sim/db/schema' +import { pendingCredentialDraft } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, lt } from 'drizzle-orm' @@ -8,6 +8,7 @@ import { createCredentialDraftContract } from '@/lib/api/contracts/credentials' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getCredentialActorContext } from '@/lib/credentials/access' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CredentialDraftAPI') @@ -33,22 +34,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } if (credentialId) { - const [membership] = await db - .select({ role: credentialMember.role, status: credentialMember.status }) - .from(credentialMember) - .innerJoin(credential, eq(credential.id, credentialMember.credentialId)) - .where( - and( - eq(credentialMember.credentialId, credentialId), - eq(credentialMember.userId, userId), - eq(credentialMember.status, 'active'), - eq(credentialMember.role, 'admin'), - eq(credential.workspaceId, workspaceId) - ) - ) - .limit(1) - - if (!membership) { + const access = await getCredentialActorContext(credentialId, userId, { workspaceAccess }) + if (!access.credential || access.credential.workspaceId !== workspaceId || !access.isAdmin) { return NextResponse.json( { error: 'Admin access required on the target credential' }, { status: 403 } diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 3b964b18e0b..318b905987b 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -4,7 +4,7 @@ import { account, credential, credentialMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray, isNotNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createWorkspaceCredentialContract, @@ -17,6 +17,11 @@ import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + getCredentialActorContext, + isSharedCredentialType, + SHARED_CREDENTIAL_TYPES, +} from '@/lib/credentials/access' import { AtlassianValidationError, normalizeAtlassianDomain, @@ -228,7 +233,16 @@ export const GET = withRouteHandler(async (request: NextRequest) => { whereClauses.push(eq(credential.providerId, providerId)) } - const credentials = await db + const isWorkspaceAdmin = workspaceAccess.canAdmin + const accessClause = isWorkspaceAdmin + ? or( + isNotNull(credentialMember.id), + inArray(credential.type, SHARED_CREDENTIAL_TYPES), + eq(credential.envOwnerUserId, session.user.id) + ) + : or(isNotNull(credentialMember.id), eq(credential.envOwnerUserId, session.user.id)) + + const rows = await db .select({ id: credential.id, workspaceId: credential.workspaceId, @@ -242,10 +256,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { createdBy: credential.createdBy, createdAt: credential.createdAt, updatedAt: credential.updatedAt, - role: credentialMember.role, + memberRole: credentialMember.role, }) .from(credential) - .innerJoin( + .leftJoin( credentialMember, and( eq(credentialMember.credentialId, credential.id), @@ -253,7 +267,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { eq(credentialMember.status, 'active') ) ) - .where(and(...whereClauses)) + .where(and(...whereClauses, accessClause)) + + const credentials = rows.map(({ memberRole, ...rest }) => ({ + ...rest, + role: + isWorkspaceAdmin && isSharedCredentialType(rest.type) ? 'admin' : (memberRole ?? 'member'), + })) return NextResponse.json({ credentials }) } catch (error) { @@ -440,29 +460,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if (existingCredential) { - const [membership] = await db - .select({ - id: credentialMember.id, - status: credentialMember.status, - role: credentialMember.role, - }) - .from(credentialMember) - .where( - and( - eq(credentialMember.credentialId, existingCredential.id), - eq(credentialMember.userId, session.user.id) - ) - ) - .limit(1) + const access = await getCredentialActorContext(existingCredential.id, session.user.id, { + workspaceAccess, + }) - if (!membership || membership.status !== 'active') { + if (!access.member && !access.isAdmin) { return NextResponse.json( { error: 'A credential with this source already exists in this workspace' }, { status: 409 } ) } - const canUpdateExistingCredential = membership.role === 'admin' + const canUpdateExistingCredential = access.isAdmin const shouldUpdateDisplayName = type === 'oauth' && resolvedDisplayName && @@ -498,11 +507,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const now = new Date() const credentialId = generateId() - const { - ownerId: workspaceOwnerId, - memberUserIds: workspaceMemberUserIds, - adminUserIds: workspaceAdminUserIds, - } = await getWorkspaceMembership(workspaceId) + const { ownerId: workspaceOwnerId, memberUserIds: workspaceMemberUserIds } = + await getWorkspaceMembership(workspaceId) await db.transaction(async (tx) => { // service_account has no DB-level unique index on (workspaceId, providerId, @@ -537,8 +543,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if ((type === 'env_workspace' || type === 'service_account') && workspaceOwnerId) { if (workspaceMemberUserIds.length > 0) { for (const memberUserId of workspaceMemberUserIds) { - const isAdmin = - memberUserId === session.user.id || workspaceAdminUserIds.has(memberUserId) + const isAdmin = memberUserId === session.user.id await tx.insert(credentialMember).values({ id: generateId(), credentialId, diff --git a/apps/sim/app/api/files/authorization.ts b/apps/sim/app/api/files/authorization.ts index 7b96031baf8..8e0c66b4173 100644 --- a/apps/sim/app/api/files/authorization.ts +++ b/apps/sim/app/api/files/authorization.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { document, knowledgeBase, workspaceFile } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { permissionSatisfies } from '@sim/platform-authz/workspace' import { and, eq, isNull } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getFileMetadata } from '@/lib/uploads' @@ -39,8 +40,7 @@ function workspacePermissionSatisfies( permission: WorkspacePermission | null, requireWrite: boolean ): boolean { - if (permission === null) return false - return requireWrite ? permission === 'write' || permission === 'admin' : true + return permissionSatisfies(permission, requireWrite ? 'write' : 'read') } /** diff --git a/apps/sim/app/api/folders/[id]/duplicate/route.ts b/apps/sim/app/api/folders/[id]/duplicate/route.ts index cc02764e2f4..5e0df625558 100644 --- a/apps/sim/app/api/folders/[id]/duplicate/route.ts +++ b/apps/sim/app/api/folders/[id]/duplicate/route.ts @@ -2,8 +2,8 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { FolderLockedError } from '@sim/platform-authz/workflow' import { generateId } from '@sim/utils/id' -import { FolderLockedError } from '@sim/workflow-authz' import { and, eq, isNull, min } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { duplicateFolderContract } from '@/lib/api/contracts' diff --git a/apps/sim/app/api/folders/[id]/route.ts b/apps/sim/app/api/folders/[id]/route.ts index 26d5f218005..48483258eb1 100644 --- a/apps/sim/app/api/folders/[id]/route.ts +++ b/apps/sim/app/api/folders/[id]/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { assertFolderMutable, FolderLockedError } from '@sim/workflow-authz' +import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateFolderContract } from '@/lib/api/contracts' diff --git a/apps/sim/app/api/folders/reorder/route.ts b/apps/sim/app/api/folders/reorder/route.ts index 274e2bc7784..b361abf6df1 100644 --- a/apps/sim/app/api/folders/reorder/route.ts +++ b/apps/sim/app/api/folders/reorder/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { assertFolderMutable, FolderLockedError } from '@sim/workflow-authz' +import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow' import { eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { reorderFoldersContract } from '@/lib/api/contracts' diff --git a/apps/sim/app/api/folders/route.test.ts b/apps/sim/app/api/folders/route.test.ts index baafe6fd2ad..f7eb512da68 100644 --- a/apps/sim/app/api/folders/route.test.ts +++ b/apps/sim/app/api/folders/route.test.ts @@ -394,7 +394,7 @@ describe('Folders API Route', () => { it('should reject creating a subfolder inside a locked parent folder', async () => { mockAuthenticatedUser() - const { FolderLockedError } = await import('@sim/workflow-authz') + const { FolderLockedError } = await import('@sim/platform-authz/workflow') workflowAuthzMockFns.mockAssertFolderMutable.mockRejectedValueOnce( new FolderLockedError('Folder is locked') ) diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts index f359e376542..d9206b0caeb 100644 --- a/apps/sim/app/api/folders/route.ts +++ b/apps/sim/app/api/folders/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { assertFolderMutable, FolderLockedError } from '@sim/workflow-authz' +import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow' import { and, asc, eq, isNotNull, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createFolderContract, listFoldersContract } from '@/lib/api/contracts' diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index c2754b79db9..ca50b20b2d2 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { type NextRequest, NextResponse } from 'next/server' import { guardrailsValidateContract } from '@/lib/api/contracts' import { parseRequest } from '@/lib/api/server' diff --git a/apps/sim/app/api/jobs/[jobId]/route.test.ts b/apps/sim/app/api/jobs/[jobId]/route.test.ts index 0dceacd56eb..189e03b1052 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.test.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.test.ts @@ -15,7 +15,7 @@ vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: mockGetJobQueue, })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflow, })) diff --git a/apps/sim/app/api/jobs/[jobId]/route.ts b/apps/sim/app/api/jobs/[jobId]/route.ts index adba3ec4d5d..01677e506ee 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.ts @@ -37,7 +37,9 @@ export const GET = withRouteHandler( const metadataToCheck = job.metadata if (metadataToCheck?.workflowId) { - const { authorizeWorkflowByWorkspacePermission } = await import('@sim/workflow-authz') + const { authorizeWorkflowByWorkspacePermission } = await import( + '@sim/platform-authz/workflow' + ) const accessCheck = await authorizeWorkflowByWorkspacePermission({ userId: authenticatedUserId, workflowId: metadataToCheck.workflowId as string, diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts index 8e80e41f6e3..059abee2fb6 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { bulkKnowledgeChunksContract, diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 371fc1512c7..dd10a328d27 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -1,8 +1,8 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { bulkKnowledgeDocumentsContract, diff --git a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts index 40220dd0732..d1c3af79f73 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts @@ -2,9 +2,9 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { document } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { upsertKnowledgeDocumentContract } from '@/lib/api/contracts/knowledge' diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 9dd40280f82..021f3059e36 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { knowledgeSearchBodySchema } from '@/lib/api/contracts/knowledge' import { parseJsonBody, validationErrorResponse } from '@/lib/api/server' diff --git a/apps/sim/app/api/logs/execution/[executionId]/route.ts b/apps/sim/app/api/logs/execution/[executionId]/route.ts index adab287bf99..82e33a644c9 100644 --- a/apps/sim/app/api/logs/execution/[executionId]/route.ts +++ b/apps/sim/app/api/logs/execution/[executionId]/route.ts @@ -1,13 +1,12 @@ import { db } from '@sim/db' import { jobExecutionLogs, - permissions, workflow, workflowExecutionLogs, workflowExecutionSnapshots, } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, inArray } from 'drizzle-orm' +import { eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { executionIdParamsSchema } from '@/lib/api/contracts/logs' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' @@ -15,6 +14,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('LogsByExecutionIdAPI') @@ -52,22 +52,23 @@ export const GET = withRouteHandler( }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, authenticatedUserId) - ) - ) .where(eq(workflowExecutionLogs.executionId, executionId)) .limit(1) + if ( + workflowLog && + !(await checkWorkspaceAccess(workflowLog.workspaceId, authenticatedUserId)).hasAccess + ) { + logger.warn(`[${requestId}] Execution access denied: ${executionId}`) + return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 }) + } + // Fallback: check job_execution_logs if (!workflowLog) { const [jobLog] = await db .select({ id: jobExecutionLogs.id, + workspaceId: jobExecutionLogs.workspaceId, executionId: jobExecutionLogs.executionId, trigger: jobExecutionLogs.trigger, startedAt: jobExecutionLogs.startedAt, @@ -77,18 +78,13 @@ export const GET = withRouteHandler( executionData: jobExecutionLogs.executionData, }) .from(jobExecutionLogs) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, jobExecutionLogs.workspaceId), - eq(permissions.userId, authenticatedUserId) - ) - ) .where(eq(jobExecutionLogs.executionId, executionId)) .limit(1) - if (!jobLog) { + if ( + !jobLog || + !(await checkWorkspaceAccess(jobLog.workspaceId, authenticatedUserId)).hasAccess + ) { logger.warn(`[${requestId}] Execution not found or access denied: ${executionId}`) return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 }) } diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index 560eee71618..a2006538fd9 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -1,5 +1,5 @@ import { dbReplica } from '@sim/db' -import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema' +import { workflow, workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' @@ -10,6 +10,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('LogsExportAPI') @@ -72,6 +73,18 @@ export const GET = withRouteHandler(async (request: NextRequest) => { 'traceSpans', ].join(',') + const access = await checkWorkspaceAccess(params.workspaceId, userId) + if (!access.hasAccess) { + return new NextResponse(`${header}\n`, { + status: 200, + headers: { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': 'attachment; filename="logs-export.csv"', + 'Cache-Control': 'no-cache', + }, + }) + } + const encoder = new TextEncoder() const stream = new ReadableStream({ start: async (controller) => { @@ -84,14 +97,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { .select(selectColumns) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(conditions) .orderBy(desc(workflowExecutionLogs.startedAt)) .limit(pageSize) diff --git a/apps/sim/app/api/logs/stats/route.ts b/apps/sim/app/api/logs/stats/route.ts index 359a8b7505d..88f33ff6b54 100644 --- a/apps/sim/app/api/logs/stats/route.ts +++ b/apps/sim/app/api/logs/stats/route.ts @@ -1,5 +1,5 @@ import { dbReplica } from '@sim/db' -import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema' +import { workflow, workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' @@ -15,6 +15,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildFilterConditions } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('LogsStatsAPI') @@ -36,6 +37,22 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const { searchParams } = new URL(request.url) const params = statsQueryParamsSchema.parse(Object.fromEntries(searchParams.entries())) + const access = await checkWorkspaceAccess(params.workspaceId, userId) + if (!access.hasAccess) { + return NextResponse.json( + { + workflows: [], + aggregateSegments: [], + totalRuns: 0, + totalErrors: 0, + avgLatency: 0, + timeBounds: { start: new Date().toISOString(), end: new Date().toISOString() }, + segmentMs: 0, + } satisfies DashboardStatsResponse, + { status: 200 } + ) + } + const workspaceFilter = eq(workflowExecutionLogs.workspaceId, params.workspaceId) if (params.folderIds) { @@ -55,14 +72,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(whereCondition) const bounds = boundsQuery[0] @@ -103,14 +112,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(whereCondition) .groupBy( sql`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`, diff --git a/apps/sim/app/api/logs/triggers/route.ts b/apps/sim/app/api/logs/triggers/route.ts index 1ebe834b6f9..2b033384eca 100644 --- a/apps/sim/app/api/logs/triggers/route.ts +++ b/apps/sim/app/api/logs/triggers/route.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { permissions, workflowExecutionLogs } from '@sim/db/schema' +import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNotNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' @@ -8,6 +8,7 @@ import { searchParamsToObject, validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('TriggersAPI') @@ -40,19 +41,16 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const params = validation.data + const access = await checkWorkspaceAccess(params.workspaceId, userId) + if (!access.hasAccess) { + return NextResponse.json({ triggers: [], count: 0 }) + } + const triggers = await db .selectDistinct({ trigger: workflowExecutionLogs.trigger, }) .from(workflowExecutionLogs) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where( and( eq(workflowExecutionLogs.workspaceId, params.workspaceId), diff --git a/apps/sim/app/api/mcp/discover/route.ts b/apps/sim/app/api/mcp/discover/route.ts index 5c63714b0a0..222a50e70d4 100644 --- a/apps/sim/app/api/mcp/discover/route.ts +++ b/apps/sim/app/api/mcp/discover/route.ts @@ -1,11 +1,12 @@ import { db } from '@sim/db' -import { permissions, workflowMcpServer, workspace } from '@sim/db/schema' +import { workflowMcpServer, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { checkHybridAuth } from '@/lib/auth/hybrid' import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' const logger = createLogger('McpDiscoverAPI') @@ -34,24 +35,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ) } - const userWorkspacePermissions = await db - .select({ entityId: permissions.entityId }) - .from(permissions) - .innerJoin(workspace, eq(permissions.entityId, workspace.id)) - .where( - and( - eq(permissions.userId, userId), - eq(permissions.entityType, 'workspace'), - isNull(workspace.archivedAt) - ) - ) + const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId) + const accessibleWorkspaceIds = accessibleRows.map((row) => row.workspace.id) const workspaceIds = auth.apiKeyType === 'workspace' && auth.workspaceId - ? userWorkspacePermissions - .map((w) => w.entityId) - .filter((workspaceId) => workspaceId === auth.workspaceId) - : userWorkspacePermissions.map((w) => w.entityId) + ? accessibleWorkspaceIds.filter((workspaceId) => workspaceId === auth.workspaceId) + : accessibleWorkspaceIds if (workspaceIds.length === 0) { return NextResponse.json({ success: true, servers: [] }) diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index afbd00e78ca..0f954023a2d 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -10,6 +10,7 @@ import { workspace, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { getErrorMessage } from '@sim/utils/errors' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' @@ -79,7 +80,7 @@ export const GET = withRouteHandler( } const userRole = memberEntry.role - if (!['owner', 'admin'].includes(userRole)) { + if (!isOrgAdminRole(userRole)) { return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 }) } @@ -149,7 +150,7 @@ export const POST = withRouteHandler( ) } - if (!['owner', 'admin'].includes(memberEntry.role)) { + if (!isOrgAdminRole(memberEntry.role)) { return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 }) } diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts index f6f3dd68944..69433ca0b7d 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db, dbReplica } from '@sim/db' import { member, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateOrganizationMemberRoleContract } from '@/lib/api/contracts/organization' @@ -54,7 +55,7 @@ export const GET = withRouteHandler( } const userRole = userMember[0].role - const hasAdminAccess = ['owner', 'admin'].includes(userRole) + const hasAdminAccess = isOrgAdminRole(userRole) const memberQuery = db .select({ @@ -182,7 +183,7 @@ export const PUT = withRouteHandler( ) } - if (!['owner', 'admin'].includes(userMember[0].role)) { + if (!isOrgAdminRole(userMember[0].role)) { return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 }) } @@ -306,7 +307,7 @@ export const DELETE = withRouteHandler( } const canRemoveMembers = - ['owner', 'admin'].includes(userMember[0].role) || session.user.id === targetUserId + isOrgAdminRole(userMember[0].role) || session.user.id === targetUserId if (!canRemoveMembers) { return NextResponse.json({ error: 'Forbidden - Insufficient permissions' }, { status: 403 }) diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts index e92f8e1ce6a..fcfb90ebc62 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts @@ -10,6 +10,7 @@ const { mockGetOrgMemberUsageLimit, mockGetOrgMemberWorkspaceUsage, mockSetOrgMemberUsageLimit, + mockGetOrganizationSubscription, mockFlags, } = vi.hoisted(() => ({ mockGetSession: vi.fn(), @@ -17,6 +18,7 @@ const { mockGetOrgMemberUsageLimit: vi.fn(), mockGetOrgMemberWorkspaceUsage: vi.fn(), mockSetOrgMemberUsageLimit: vi.fn(), + mockGetOrganizationSubscription: vi.fn(), mockFlags: { isHosted: true }, })) @@ -37,6 +39,10 @@ vi.mock('@/lib/billing/organizations/member-limits', () => ({ setOrgMemberUsageLimit: mockSetOrgMemberUsageLimit, })) +vi.mock('@/lib/billing/core/billing', () => ({ + getOrganizationSubscription: mockGetOrganizationSubscription, +})) + vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return mockFlags.isHosted @@ -65,6 +71,7 @@ describe('GET /api/organizations/[id]/members/[memberId]/usage-limit', () => { mockIsOrganizationOwnerOrAdmin.mockResolvedValue(true) mockGetOrgMemberWorkspaceUsage.mockResolvedValue(1) // $1 -> 200 credits mockGetOrgMemberUsageLimit.mockResolvedValue(2) // $2 -> 400 credits + mockGetOrganizationSubscription.mockResolvedValue(null) }) it('returns 401 without a session', async () => { @@ -93,6 +100,7 @@ describe('GET /api/organizations/[id]/members/[memberId]/usage-limit', () => { data: { creditsUsed: 200, creditLimit: 400, + billingInterval: 'month', }, }) }) @@ -103,6 +111,20 @@ describe('GET /api/organizations/[id]/members/[memberId]/usage-limit', () => { const body = await res.json() expect(body.data.creditLimit).toBeNull() }) + + it('reports a yearly billing interval from subscription metadata', async () => { + mockGetOrganizationSubscription.mockResolvedValue({ metadata: { billingInterval: 'year' } }) + const res = await GET(getRequest(), context()) + const body = await res.json() + expect(body.data.billingInterval).toBe('year') + }) + + it('prefers the billing_interval column when metadata lacks it', async () => { + mockGetOrganizationSubscription.mockResolvedValue({ billingInterval: 'year', metadata: {} }) + const res = await GET(getRequest(), context()) + const body = await res.json() + expect(body.data.billingInterval).toBe('year') + }) }) describe('PUT /api/organizations/[id]/members/[memberId]/usage-limit', () => { diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts index ad85d7ef83e..153da1db116 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts @@ -7,7 +7,9 @@ import { } from '@/lib/api/contracts/organization' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' +import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization' +import { resolveBillingInterval } from '@/lib/billing/core/subscription' import { creditsToDollars, dollarsToCredits } from '@/lib/billing/credits/conversion' import { getOrgMemberUsageLimit, @@ -48,9 +50,10 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 }) } - const [usage, limitDollars] = await Promise.all([ + const [usage, limitDollars, orgSubscription] = await Promise.all([ getOrgMemberWorkspaceUsage(organizationId, memberId), getOrgMemberUsageLimit(organizationId, memberId), + getOrganizationSubscription(organizationId), ]) return NextResponse.json({ @@ -58,6 +61,7 @@ export const GET = withRouteHandler( data: { creditsUsed: dollarsToCredits(usage), creditLimit: limitDollars === null ? null : dollarsToCredits(limitDollars), + billingInterval: resolveBillingInterval(orgSubscription), }, }) } diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index a8b23088fa1..9482c76ac9b 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -8,6 +8,7 @@ import { userStats, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { @@ -82,7 +83,7 @@ export const GET = withRouteHandler( } const userRole = memberEntry[0].role - const hasAdminAccess = ['owner', 'admin'].includes(userRole) + const hasAdminAccess = isOrgAdminRole(userRole) // Get organization members const query = db @@ -234,7 +235,7 @@ export const POST = withRouteHandler( ) } - if (!['owner', 'admin'].includes(memberEntry[0].role)) { + if (!isOrgAdminRole(memberEntry[0].role)) { return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 }) } diff --git a/apps/sim/app/api/organizations/[id]/roster/route.ts b/apps/sim/app/api/organizations/[id]/roster/route.ts index 8ccdcfe6227..fe8dc0edbc2 100644 --- a/apps/sim/app/api/organizations/[id]/roster/route.ts +++ b/apps/sim/app/api/organizations/[id]/roster/route.ts @@ -8,6 +8,7 @@ import { workspace, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { organizationParamsSchema } from '@/lib/api/contracts/organization' @@ -117,16 +118,25 @@ export const GET = withRouteHandler( permissionsByUser.set(row.userId, list) } - const members = memberRows.map((row) => ({ - memberId: row.memberId, - userId: row.userId, - role: row.role, - createdAt: row.createdAt, - name: row.userName, - email: row.userEmail, - image: row.userImage, - workspaces: permissionsByUser.get(row.userId) ?? [], - })) + const members = memberRows.map((row) => { + const isOrgAdmin = isOrgAdminRole(row.role) + return { + memberId: row.memberId, + userId: row.userId, + role: row.role, + createdAt: row.createdAt, + name: row.userName, + email: row.userEmail, + image: row.userImage, + workspaces: isOrgAdmin + ? orgWorkspaces.map((ws) => ({ + workspaceId: ws.id, + workspaceName: ws.name, + permission: 'admin' as const, + })) + : (permissionsByUser.get(row.userId) ?? []), + } + }) const externalPermissionRows = orgWorkspaceIds.length > 0 diff --git a/apps/sim/app/api/organizations/[id]/route.ts b/apps/sim/app/api/organizations/[id]/route.ts index d44cd97e1c0..ba074c3ab5f 100644 --- a/apps/sim/app/api/organizations/[id]/route.ts +++ b/apps/sim/app/api/organizations/[id]/route.ts @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, organization } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, ne } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateOrganizationContract } from '@/lib/api/contracts/organization' @@ -73,7 +74,7 @@ export const GET = withRouteHandler( } const userRole = memberEntry[0].role - const hasAdminAccess = ['owner', 'admin'].includes(userRole) + const hasAdminAccess = isOrgAdminRole(userRole) const response: OrganizationDetailsResponse = { success: true, @@ -148,7 +149,7 @@ export const PUT = withRouteHandler( ) } - if (!['owner', 'admin'].includes(memberEntry[0].role)) { + if (!isOrgAdminRole(memberEntry[0].role)) { return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 }) } diff --git a/apps/sim/app/api/organizations/route.ts b/apps/sim/app/api/organizations/route.ts index 5a2aaabb2d2..ba1fece2f3d 100644 --- a/apps/sim/app/api/organizations/route.ts +++ b/apps/sim/app/api/organizations/route.ts @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, organization, subscription as subscriptionTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { getErrorMessage } from '@sim/utils/errors' import { and, eq, inArray, or } from 'drizzle-orm' import type { NextRequest } from 'next/server' @@ -113,7 +114,7 @@ export const POST = withRouteHandler(async (request: Request) => { .limit(1) const existingAdminMembership = - existingOrgMembership.length > 0 && ['owner', 'admin'].includes(existingOrgMembership[0].role) + existingOrgMembership.length > 0 && isOrgAdminRole(existingOrgMembership[0].role) ? existingOrgMembership[0] : null diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 62455bb85d4..56949815250 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -6,7 +6,7 @@ import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getScheduleByIdContract, updateScheduleContract } from '@/lib/api/contracts/schedules' diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index ac7fc85ce5c..ca25b2fe946 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createScheduleContract, scheduleQuerySchema } from '@/lib/api/contracts/schedules' diff --git a/apps/sim/app/api/table/utils.ts b/apps/sim/app/api/table/utils.ts index 33986a2964e..7424258ad0e 100644 --- a/apps/sim/app/api/table/utils.ts +++ b/apps/sim/app/api/table/utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { permissionSatisfies } from '@sim/platform-authz/workspace' import { toError } from '@sim/utils/errors' import { NextResponse } from 'next/server' import { @@ -170,7 +171,7 @@ async function checkTableWriteAccess(tableId: string, userId: string): Promise { const params = parsed.data.query - const scopeError = await checkWorkspaceScope(rateLimit, params.workspaceId) - if (scopeError) return scopeError + const accessError = await validateWorkspaceAccess(rateLimit, userId, params.workspaceId, 'read') + if (accessError) return accessError logger.info(`[${requestId}] Fetching logs for workspace ${params.workspaceId}`, { userId, @@ -121,14 +121,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) const logs = await baseQuery .where(conditions) diff --git a/apps/sim/app/api/v1/middleware.ts b/apps/sim/app/api/v1/middleware.ts index 94cacfd27f8..51d69070f32 100644 --- a/apps/sim/app/api/v1/middleware.ts +++ b/apps/sim/app/api/v1/middleware.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { type PermissionType, permissionSatisfies } from '@sim/platform-authz/workspace' import { type NextRequest, NextResponse } from 'next/server' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import type { SubscriptionPlan } from '@/lib/core/rate-limiter' @@ -192,9 +193,6 @@ export async function checkWorkspaceScope( return null } -/** Orders workspace permission levels for at-least comparisons. */ -const PERMISSION_RANK = { read: 0, write: 1, admin: 2 } as const - /** * Validates workspace-scoped API key bounds and the user's workspace permission. * Returns null on success, NextResponse on failure. @@ -203,13 +201,13 @@ export async function validateWorkspaceAccess( rateLimit: RateLimitResult, userId: string, workspaceId: string, - level: keyof typeof PERMISSION_RANK = 'read' + level: PermissionType = 'read' ): Promise { const scopeError = await checkWorkspaceScope(rateLimit, workspaceId) if (scopeError) return scopeError const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - if (permission === null || PERMISSION_RANK[permission] < PERMISSION_RANK[level]) { + if (!permissionSatisfies(permission, level)) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } return null diff --git a/apps/sim/app/api/v1/workflows/[id]/deploy/route.test.ts b/apps/sim/app/api/v1/workflows/[id]/deploy/route.test.ts index 3dead585727..dd78899e2ac 100644 --- a/apps/sim/app/api/v1/workflows/[id]/deploy/route.test.ts +++ b/apps/sim/app/api/v1/workflows/[id]/deploy/route.test.ts @@ -5,8 +5,9 @@ * workspace admin permission enforcement, optional body handling, and the * mapping of orchestration results to v1 API responses. */ + +import { WorkflowLockedError } from '@sim/platform-authz/workflow' import { createMockRequest, workflowAuthzMockFns } from '@sim/testing' -import { WorkflowLockedError } from '@sim/workflow-authz' import { NextRequest, NextResponse } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' diff --git a/apps/sim/app/api/v1/workflows/[id]/deploy/route.ts b/apps/sim/app/api/v1/workflows/[id]/deploy/route.ts index 34982534db9..008a45a9820 100644 --- a/apps/sim/app/api/v1/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/v1/workflows/[id]/deploy/route.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { v1DeployWorkflowBodySchema, diff --git a/apps/sim/app/api/v1/workflows/[id]/rollback/route.test.ts b/apps/sim/app/api/v1/workflows/[id]/rollback/route.test.ts index c1f085faf02..cdebe9e35af 100644 --- a/apps/sim/app/api/v1/workflows/[id]/rollback/route.test.ts +++ b/apps/sim/app/api/v1/workflows/[id]/rollback/route.test.ts @@ -5,8 +5,9 @@ * resolution (previous version by default, explicit version when provided) * and the mapping of activation results to v1 API responses. */ + +import { WorkflowLockedError } from '@sim/platform-authz/workflow' import { createMockRequest, workflowAuthzMockFns } from '@sim/testing' -import { WorkflowLockedError } from '@sim/workflow-authz' import { NextResponse } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' diff --git a/apps/sim/app/api/v1/workflows/[id]/rollback/route.ts b/apps/sim/app/api/v1/workflows/[id]/rollback/route.ts index c35933d2773..534e7fd89de 100644 --- a/apps/sim/app/api/v1/workflows/[id]/rollback/route.ts +++ b/apps/sim/app/api/v1/workflows/[id]/rollback/route.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { v1RollbackWorkflowBodySchema, diff --git a/apps/sim/app/api/v1/workflows/[id]/route.ts b/apps/sim/app/api/v1/workflows/[id]/route.ts index 584f82cd75d..653b833e591 100644 --- a/apps/sim/app/api/v1/workflows/[id]/route.ts +++ b/apps/sim/app/api/v1/workflows/[id]/route.ts @@ -1,9 +1,9 @@ import { db } from '@sim/db' import { workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getActiveWorkflowRecord } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { v1GetWorkflowContract } from '@/lib/api/contracts/v1/workflows' diff --git a/apps/sim/app/api/v1/workflows/utils.ts b/apps/sim/app/api/v1/workflows/utils.ts index 92e321f1538..89186235598 100644 --- a/apps/sim/app/api/v1/workflows/utils.ts +++ b/apps/sim/app/api/v1/workflows/utils.ts @@ -1,4 +1,4 @@ -import { type ActiveWorkflowRecord, getActiveWorkflowRecord } from '@sim/workflow-authz' +import { type ActiveWorkflowRecord, getActiveWorkflowRecord } from '@sim/platform-authz/workflow' import { NextResponse } from 'next/server' import { type RateLimitResult, validateWorkspaceAccess } from '@/app/api/v1/middleware' diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index ea79ee38f69..7ad630c5ff6 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -6,7 +6,7 @@ import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 80f34950ed2..8c7fdec2ee8 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -1,14 +1,14 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' -import { permissions, webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema' +import { webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { generateId, generateShortId } from '@sim/utils/id' import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId, generateShortId } from '@sim/utils/id' import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { listWebhooksContract, upsertWebhookContract } from '@/lib/api/contracts/webhooks' @@ -31,6 +31,7 @@ import { findConflictingWebhookPathOwner, syncWebhooksForCredentialSet, } from '@/lib/webhooks/utils.server' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' import { extractCredentialSetId, isCredentialSetValue } from '@/executor/constants' const logger = createLogger('WebhooksAPI') @@ -151,12 +152,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ webhooks: [] }, { status: 200 }) } - const workspacePermissionRows = await db - .select({ workspaceId: permissions.entityId }) - .from(permissions) - .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace'))) - - const workspaceIds = workspacePermissionRows.map((row) => row.workspaceId) + const accessibleRows = await listAccessibleWorkspaceRowsForUser(session.user.id, 'all') + const workspaceIds = accessibleRows.map((row) => row.workspace.id) if (workspaceIds.length === 0) { return NextResponse.json({ webhooks: [] }, { status: 200 }) } diff --git a/apps/sim/app/api/workflows/[id]/autolayout/route.ts b/apps/sim/app/api/workflows/[id]/autolayout/route.ts index 18d1864daef..387047da880 100644 --- a/apps/sim/app/api/workflows/[id]/autolayout/route.ts +++ b/apps/sim/app/api/workflows/[id]/autolayout/route.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' +import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { workflowAutoLayoutContract } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.ts index c8202be91dc..49d555b813d 100644 --- a/apps/sim/app/api/workflows/[id]/chat/status/route.ts +++ b/apps/sim/app/api/workflows/[id]/chat/status/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { chat } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { getChatDeploymentStatusContract } from '@/lib/api/contracts/deployments' diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 0355519c16b..75eca4dc779 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -1,7 +1,7 @@ import { db, workflow } from '@sim/db' import { createLogger } from '@sim/logger' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { updatePublicApiContract } from '@/lib/api/contracts/deployments' diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts index e7a95618bcd..1b2746f525f 100644 --- a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import type { NextRequest } from 'next/server' import { workflowDeploymentVersionParamSchema } from '@/lib/api/contracts/workflows' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts index 7ab119524d8..beba4a3f3ab 100644 --- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts +++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts @@ -1,6 +1,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' -import { FolderLockedError } from '@sim/workflow-authz' +import { FolderLockedError } from '@sim/platform-authz/workflow' import { type NextRequest, NextResponse } from 'next/server' import { duplicateWorkflowContract } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 8f3007b5c4e..13ee516f661 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -1,9 +1,9 @@ import { db } from '@sim/db' import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { getErrorMessage, toError } from '@sim/utils/errors' import { generateId, isValidUuid } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { executeWorkflowBodySchema } from '@/lib/api/contracts/workflows' diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts index 92f32a26f7d..0ca39eb6622 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts @@ -1,9 +1,9 @@ import { db } from '@sim/db' import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { cancelWorkflowExecutionContract } from '@/lib/api/contracts/workflows' diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.test.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.test.ts index 5e41a225e9e..27529007563 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.test.ts @@ -21,7 +21,7 @@ vi.mock('@/lib/auth', () => ({ getSession: mockGetSession, })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission, })) diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts index 6915a8dcbc1..2be5fed1c37 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { streamWorkflowExecutionContract } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' diff --git a/apps/sim/app/api/workflows/[id]/restore/route.ts b/apps/sim/app/api/workflows/[id]/restore/route.ts index 65a8fa96196..b42dcdb9ce4 100644 --- a/apps/sim/app/api/workflows/[id]/restore/route.ts +++ b/apps/sim/app/api/workflows/[id]/restore/route.ts @@ -1,6 +1,10 @@ import { createLogger } from '@sim/logger' +import { + assertFolderMutable, + FolderLockedError, + WorkflowLockedError, +} from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' -import { assertFolderMutable, FolderLockedError, WorkflowLockedError } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { restoreWorkflowContract } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index d42e75b67e1..d2c1f607b2d 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -7,7 +7,7 @@ import { authorizeWorkflowByWorkspacePermission, FolderLockedError, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateWorkflowContract } from '@/lib/api/contracts/workflows' diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index 5aa8010084a..fbd33b045b4 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -1,12 +1,12 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' +import { toError } from '@sim/utils/errors' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { putWorkflowNormalizedStateContract } from '@/lib/api/contracts/workflows' diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts index b2fd323324b..d6b0dd3115f 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.ts @@ -2,12 +2,12 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' +import { getErrorMessage } from '@sim/utils/errors' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { workflowVariablesContract } from '@/lib/api/contracts/workflows' diff --git a/apps/sim/app/api/workflows/middleware.test.ts b/apps/sim/app/api/workflows/middleware.test.ts index 996466426da..202326d2c15 100644 --- a/apps/sim/app/api/workflows/middleware.test.ts +++ b/apps/sim/app/api/workflows/middleware.test.ts @@ -16,7 +16,7 @@ import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@sim/workflow-authz', () => workflowAuthzMock) +vi.mock('@sim/platform-authz/workflow', () => workflowAuthzMock) vi.mock('@/lib/api-key/service', () => ({ authenticateApiKeyFromHeader: vi.fn(), updateApiKeyLastUsed: vi.fn(), diff --git a/apps/sim/app/api/workflows/middleware.ts b/apps/sim/app/api/workflows/middleware.ts index 10fa3017727..08a51fbf598 100644 --- a/apps/sim/app/api/workflows/middleware.ts +++ b/apps/sim/app/api/workflows/middleware.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import type { NextRequest } from 'next/server' import { type ApiKeyAuthResult, diff --git a/apps/sim/app/api/workflows/reorder/route.ts b/apps/sim/app/api/workflows/reorder/route.ts index 5be0f62d3e5..adb1b5416e5 100644 --- a/apps/sim/app/api/workflows/reorder/route.ts +++ b/apps/sim/app/api/workflows/reorder/route.ts @@ -8,7 +8,7 @@ import { FolderLockedError, FolderNotFoundError, WorkflowLockedError, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' import { eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { reorderWorkflowsContract } from '@/lib/api/contracts/workflows' diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index 2bfddb39343..261b8bf4b5d 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -88,7 +88,7 @@ describe('Workflows API Route - POST ordering', () => { }) it('rejects creating a workflow inside a locked folder', async () => { - const { FolderLockedError } = await import('@sim/workflow-authz') + const { FolderLockedError } = await import('@sim/platform-authz/workflow') workflowAuthzMockFns.mockAssertFolderMutable.mockRejectedValueOnce( new FolderLockedError('Folder is locked') ) diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 224cb68417d..05d94a31b1d 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { assertFolderMutable, FolderLockedError } from '@sim/workflow-authz' +import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow' import { type NextRequest, NextResponse } from 'next/server' import { createWorkflowContract, workflowListQuerySchema } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index e2a87fdfbbc..f7aad4aba15 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -44,10 +44,9 @@ const WORKSPACE_ENV_LOCK_TIMEOUT_MS = 5_000 * Restricts decrypted workspace env values to administrators. Members (including * read-only) receive the variable names with empty values so editor autocomplete * and conflict detection keep working without leaking secret values. A value is - * revealed when the caller is a credential admin of that key, or — for legacy - * keys predating per-secret ACLs — when they hold workspace `admin` permission. - * Mirrors the per-key edit gating in PUT/DELETE: if you can administer a secret, - * you can read it. + * revealed when the caller is a workspace admin (which includes organization + * admins) or a per-secret credential admin of that key. Mirrors the per-key edit + * gating in PUT/DELETE: if you can administer a secret, you can read it. */ async function maskWorkspaceEnvForViewer({ workspaceDecrypted, @@ -61,7 +60,7 @@ async function maskWorkspaceEnvForViewer({ permission: PermissionType }): Promise> { const workspaceKeys = Object.keys(workspaceDecrypted) - const { adminKeys, knownKeys } = await getWorkspaceEnvKeyAdminAccess({ + const { adminKeys } = await getWorkspaceEnvKeyAdminAccess({ workspaceId, envKeys: workspaceKeys, userId, @@ -69,7 +68,7 @@ async function maskWorkspaceEnvForViewer({ const masked: Record = {} for (const key of workspaceKeys) { - const canViewValue = adminKeys.has(key) || (!knownKeys.has(key) && permission === 'admin') + const canViewValue = permission === 'admin' || adminKeys.has(key) masked[key] = canViewValue ? workspaceDecrypted[key] : '' } return masked @@ -169,7 +168,8 @@ export const PUT = withRouteHandler( envKeys: incomingKeys, userId, }) - const forbiddenExisting = incomingKeys.filter((k) => knownKeys.has(k) && !adminKeys.has(k)) + const isKeyAdmin = (key: string) => permission === 'admin' || adminKeys.has(key) + const forbiddenExisting = incomingKeys.filter((k) => knownKeys.has(k) && !isKeyAdmin(k)) if (forbiddenExisting.length > 0) { logger.warn(`[${requestId}] Workspace env update denied`, { workspaceId, @@ -311,7 +311,8 @@ export const DELETE = withRouteHandler( envKeys: keys, userId, }) - const forbiddenExisting = keys.filter((k) => knownKeys.has(k) && !adminKeys.has(k)) + const isKeyAdmin = (key: string) => permission === 'admin' || adminKeys.has(key) + const forbiddenExisting = keys.filter((k) => knownKeys.has(k) && !isKeyAdmin(k)) if (forbiddenExisting.length > 0) { logger.warn(`[${requestId}] Workspace env delete denied`, { workspaceId, diff --git a/apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts b/apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts index 2bf216a930f..548a6939c43 100644 --- a/apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts +++ b/apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts @@ -1,11 +1,12 @@ -import { db, dbReplica } from '@sim/db' -import { pausedExecutions, permissions, workflow, workflowExecutionLogs } from '@sim/db/schema' +import { dbReplica } from '@sim/db' +import { pausedExecutions, workflow, workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { workspaceMetricsExecutionsQuerySchema } from '@/lib/api/contracts/workspaces' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('MetricsExecutionsAPI') @@ -36,18 +37,8 @@ export const GET = withRouteHandler( const segments = qp.segments - const [permission] = await db - .select() - .from(permissions) - .where( - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspaceId), - eq(permissions.userId, userId) - ) - ) - .limit(1) - if (!permission) { + const access = await checkWorkspaceAccess(workspaceId, userId) + if (!access.hasAccess) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } const wfWhere = [eq(workflow.workspaceId, workspaceId)] as any[] diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts index 8e9bbd5bf32..43a450765b5 100644 --- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts @@ -1,9 +1,10 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' -import { permissions, user, workspace, workspaceEnvironment } from '@sim/db/schema' +import { member, permissions, user, workspace, workspaceEnvironment } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { ORG_ADMIN_ROLES } from '@sim/platform-authz/workspace' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateWorkspacePermissionsContract } from '@/lib/api/contracts/workspaces' import { parseRequest } from '@/lib/api/server' @@ -112,7 +113,10 @@ export const PATCH = withRouteHandler( const body = parsed.data.body const workspaceRow = await db - .select({ billedAccountUserId: workspace.billedAccountUserId }) + .select({ + billedAccountUserId: workspace.billedAccountUserId, + organizationId: workspace.organizationId, + }) .from(workspace) .where(eq(workspace.id, workspaceId)) .limit(1) @@ -122,6 +126,27 @@ export const PATCH = withRouteHandler( } const billedAccountUserId = workspaceRow[0].billedAccountUserId + const organizationId = workspaceRow[0].organizationId + + if (organizationId) { + const targetUserIds = body.updates.map((update) => update.userId) + const orgAdminTargets = await db + .select({ userId: member.userId }) + .from(member) + .where( + and( + eq(member.organizationId, organizationId), + inArray(member.userId, targetUserIds), + inArray(member.role, [...ORG_ADMIN_ROLES]) + ) + ) + if (orgAdminTargets.length > 0) { + return NextResponse.json( + { error: 'Organization admins are workspace admins and their role cannot be changed' }, + { status: 400 } + ) + } + } const selfUpdate = body.updates.find((update) => update.userId === session.user.id) if (selfUpdate && selfUpdate.permissions !== 'admin') { diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index 1e8bbac5b82..1b215792d94 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -14,7 +14,10 @@ const logger = createLogger('WorkspaceByIdAPI') import { db } from '@sim/db' import { permissions, workspace } from '@sim/db/schema' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { + getEffectiveWorkspacePermission, + getUserEntityPermissions, +} from '@/lib/workspaces/permissions/utils' export const GET = withRouteHandler( async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => { @@ -140,28 +143,11 @@ export const PATCH = withRouteHandler( const candidateId = billedAccountUserId - const isOwner = candidateId === existingWorkspace.ownerId - - let hasAdminAccess = isOwner - - if (!hasAdminAccess) { - const adminPermission = await db - .select({ id: permissions.id }) - .from(permissions) - .where( - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspaceId), - eq(permissions.userId, candidateId), - eq(permissions.permissionType, 'admin') - ) - ) - .limit(1) - - hasAdminAccess = adminPermission.length > 0 - } - - if (!hasAdminAccess) { + const candidatePermission = await getEffectiveWorkspacePermission( + candidateId, + existingWorkspace + ) + if (candidatePermission !== 'admin') { return NextResponse.json( { error: 'Billed account must be a workspace admin' }, { status: 400 } diff --git a/apps/sim/app/api/workspaces/invitations/route.test.ts b/apps/sim/app/api/workspaces/invitations/route.test.ts index c364d8228e4..ddfe8a59213 100644 --- a/apps/sim/app/api/workspaces/invitations/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/route.test.ts @@ -124,6 +124,7 @@ describe('POST /api/workspaces/invitations/batch', () => { workspaceMode: 'grandfathered_shared', billedAccountUserId: 'user-1', }) + permissionsMockFns.mockHasWorkspaceAdminAccess.mockResolvedValue(true) mockValidateInvitationsAllowed.mockResolvedValue(undefined) mockGetWorkspaceInvitePolicy.mockResolvedValue({ allowed: true, @@ -164,7 +165,7 @@ describe('POST /api/workspaces/invitations/batch', () => { organizationId: null, upgradeRequired: true, }) - mockDbResults.value = [[{ permissionType: 'admin' }]] + mockDbResults.value = [] const request = createMockRequest('POST', { workspaceId: 'workspace-1', @@ -195,7 +196,7 @@ describe('POST /api/workspaces/invitations/batch', () => { organizationId: null, upgradeRequired: true, }) - mockDbResults.value = [[{ permissionType: 'admin' }]] + mockDbResults.value = [] const request = createMockRequest('POST', { workspaceId: 'workspace-1', @@ -233,7 +234,7 @@ describe('POST /api/workspaces/invitations/batch', () => { maxSeats: 5, availableSeats: 0, }) - mockDbResults.value = [[{ permissionType: 'admin' }], []] + mockDbResults.value = [[]] const request = createMockRequest('POST', { workspaceId: 'workspace-1', @@ -276,10 +277,7 @@ describe('POST /api/workspaces/invitations/batch', () => { role: 'member', memberId: 'member-1', }) - mockDbResults.value = [ - [{ permissionType: 'admin' }], - [{ id: 'existing-user', email: 'new@example.com' }], - ] + mockDbResults.value = [[{ id: 'existing-user', email: 'new@example.com' }]] const request = createMockRequest('POST', { workspaceId: 'workspace-1', @@ -313,7 +311,7 @@ describe('POST /api/workspaces/invitations/batch', () => { workspaceMode: 'grandfathered_shared', billedAccountUserId: 'user-1', }) - mockDbResults.value = [[{ permissionType: 'admin' }], []] + mockDbResults.value = [[]] const request = createMockRequest('POST', { workspaceId: 'workspace-1', @@ -338,7 +336,7 @@ describe('POST /api/workspaces/invitations/batch', () => { }) it('creates multiple workspace invitations in one batch request', async () => { - mockDbResults.value = [[{ permissionType: 'admin' }], [], []] + mockDbResults.value = [[], []] mockCreatePendingInvitation .mockResolvedValueOnce({ invitationId: 'inv-1', @@ -384,7 +382,7 @@ describe('POST /api/workspaces/invitations/batch', () => { success: false, error: 'mailer unavailable', }) - mockDbResults.value = [[{ permissionType: 'admin' }], []] + mockDbResults.value = [[]] const request = createMockRequest('POST', { workspaceId: 'workspace-1', diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 378b169ad66..101f0dc8fff 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -1,11 +1,9 @@ -import { db } from '@sim/db' -import { permissions, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { listInvitationsForWorkspaces } from '@/lib/invitations/core' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' export const dynamic = 'force-dynamic' @@ -18,24 +16,14 @@ export const GET = withRouteHandler(async (req: NextRequest) => { } try { - const userWorkspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .innerJoin( - permissions, - and( - eq(permissions.entityId, workspace.id), - eq(permissions.entityType, 'workspace'), - eq(permissions.userId, session.user.id) - ) - ) - .where(isNull(workspace.archivedAt)) - - if (userWorkspaces.length === 0) { + const accessibleRows = await listAccessibleWorkspaceRowsForUser(session.user.id) + if (accessibleRows.length === 0) { return NextResponse.json({ invitations: [] }) } - const invitations = await listInvitationsForWorkspaces(userWorkspaces.map((w) => w.id)) + const invitations = await listInvitationsForWorkspaces( + accessibleRows.map((row) => row.workspace.id) + ) return NextResponse.json({ invitations }) } catch (error) { logger.error('Error fetching workspace invitations:', error) diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index 8679aea74c5..54889076a58 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -85,16 +85,10 @@ export const DELETE = withRouteHandler( return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) } - if ( - isRemovingWorkspaceOwner && - !isSelf && - session.user.id !== workspaceRow[0].billedAccountUserId - ) { - return NextResponse.json( - { error: 'Only the workspace owner or billing account can remove the workspace owner' }, - { status: 403 } - ) - } + // Removing the workspace owner is allowed for any admin: ownership transfers + // to the billing account in the transaction below. The billing account itself + // stays protected by the guard above (and personal workspaces, where owner == + // billing account, are blocked there). // Prevent removing yourself if you're the last admin if (isSelf && userPermission?.permissionType === 'admin' && !isRemovingWorkspaceOwner) { diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index 42513fa1cca..8d35dc7429b 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -3,7 +3,7 @@ import { db } from '@sim/db' import { permissions, settings, type WorkspaceMode, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { and, desc, eq, isNull, sql } from 'drizzle-orm' +import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { listWorkspacesQuerySchema } from '@/lib/api/contracts' import { createWorkspaceContract } from '@/lib/api/contracts/workspaces' @@ -26,6 +26,7 @@ import { UPGRADE_TO_INVITE_REASON, WORKSPACE_MODE, } from '@/lib/workspaces/policy' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' const logger = createLogger('Workspaces') @@ -62,29 +63,7 @@ export const GET = withRouteHandler(async (request: Request) => { .limit(1) const [userWorkspaces, userSettings] = await Promise.all([ - db - .select({ - workspace: workspace, - permissionType: permissions.permissionType, - }) - .from(permissions) - .innerJoin(workspace, eq(permissions.entityId, workspace.id)) - .where( - scope === 'all' - ? and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')) - : scope === 'archived' - ? and( - eq(permissions.userId, session.user.id), - eq(permissions.entityType, 'workspace'), - sql`${workspace.archivedAt} IS NOT NULL` - ) - : and( - eq(permissions.userId, session.user.id), - eq(permissions.entityType, 'workspace'), - isNull(workspace.archivedAt) - ) - ) - .orderBy(desc(workspace.createdAt)), + listAccessibleWorkspaceRowsForUser(session.user.id, scope), settingsQuery, ]) diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx index 6177c1bf8a1..a9067b55f1d 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { Avatar, AvatarFallback, Chip, ChipDropdown } from '@/components/emcn' +import { credentialRoleLockReason, RoleLockTooltip } from '@/components/permissions' import { cn } from '@/lib/core/utils/cn' import { getUserColor } from '@/lib/workspaces/colors' import { @@ -32,7 +33,9 @@ export function CredentialMembersSection({ credentialId, isAdmin }: CredentialMe const removeMember = useRemoveWorkspaceCredentialMember() const activeMembers = members.filter((member) => member.status === 'active') - const adminMemberCount = activeMembers.filter((member) => member.role === 'admin').length + const explicitAdminCount = activeMembers.filter( + (member) => member.role === 'admin' && member.roleSource !== 'workspace-admin' + ).length const handleChangeMemberRole = async (userId: string, role: WorkspaceCredentialRole) => { const current = activeMembers.find((member) => member.userId === userId) @@ -57,8 +60,13 @@ export function CredentialMembersSection({ credentialId, isAdmin }: CredentialMe {membersLoading ? null : (
{activeMembers.map((member) => { - const roleLocked = member.role === 'admin' && adminMemberCount <= 1 - const roleDisabled = !isAdmin || roleLocked + const lockReason = credentialRoleLockReason(member.roleSource) + const roleLocked = + member.role === 'admin' && + member.roleSource !== 'workspace-admin' && + explicitAdminCount <= 1 + const roleDisabled = !isAdmin || roleLocked || lockReason !== null + const removeDisabled = roleLocked || lockReason !== null return (
- - handleChangeMemberRole(member.userId, role as WorkspaceCredentialRole) - } - /> + + + handleChangeMemberRole(member.userId, role as WorkspaceCredentialRole) + } + /> + {isAdmin && ( handleRemoveMember(member.userId)} - disabled={roleLocked} + disabled={removeDisabled} flush className='justify-self-end' > diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx index a30e7071bb1..79c375564d3 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx @@ -1,6 +1,7 @@ 'use client' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { getErrorMessage } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' import { useParams, useRouter } from 'next/navigation' @@ -176,7 +177,7 @@ export function Billing() { const isBlocked = Boolean(subscriptionData?.data?.billingBlocked) const userRole = subscriptionData?.data?.organization?.role ?? 'member' - const isTeamAdmin = ['owner', 'admin'].includes(userRole) + const isTeamAdmin = isOrgAdminRole(userRole) const shouldUseOrganizationBillingContext = subscription.isOrgScoped && isTeamAdmin const { data: invoicesData } = useInvoices({ diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx index 9cd6c56d63f..f41d180ed6c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { Plus } from 'lucide-react' import { Avatar, @@ -63,7 +64,7 @@ export function CredentialSets() { const subscriptionAccess = getSubscriptionAccessState(subscriptionData?.data) const hasTeamPlan = subscriptionAccess.hasUsableTeamAccess const userRole = getUserRole(activeOrganization, session?.user?.email) - const isAdmin = userRole === 'admin' || userRole === 'owner' + const isAdmin = isOrgAdminRole(userRole) const canManageCredentialSets = hasTeamPlan && isAdmin && !!activeOrganization?.id const { data: memberships = [], isPending: membershipsLoading } = useCredentialSetMemberships() diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/manage-credits-modal/manage-credits-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/manage-credits-modal/manage-credits-modal.tsx index 85d48e2bd61..08b54211fa1 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/manage-credits-modal/manage-credits-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/manage-credits-modal/manage-credits-modal.tsx @@ -72,6 +72,9 @@ export function ManageCreditsModal({ const isSaving = updateLimit.isPending const creditsUsed = data ? data.creditsUsed.toLocaleString() : '—' + const creditsUsedTitle = data + ? `Credits used this ${data.billingInterval === 'year' ? 'year' : 'month'}` + : 'Credits used' const handleSave = () => { if (!userId) return @@ -97,7 +100,7 @@ export function ManageCreditsModal({ diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx index 144fa39c69c..ca12bbced71 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { getErrorMessage } from '@sim/utils/errors' import { ChipDropdown, @@ -15,7 +16,12 @@ import { Search, toast, } from '@/components/emcn' -import type { OrgRole, PermissionType } from '@/components/permissions' +import { + type OrgRole, + type PermissionType, + RoleLockTooltip, + workspaceRoleLockReason, +} from '@/components/permissions' import type { OrganizationRoster, RosterMember, @@ -305,15 +311,11 @@ export function OrganizationMemberLists({ workspaceId: string, access: RosterWorkspaceAccess ) => { - const rowUserIsOrgAdmin = member.role === 'owner' || member.role === 'admin' + const rowUserIsOrgAdmin = isOrgAdminRole(member.role) const isSelf = member.userId === currentUserId const wouldDemoteSelf = isSelf && access.permission === 'admin' const disabled = rowUserIsOrgAdmin || wouldDemoteSelf || updatePermissions.isPending - /** - * Org owners/admins keep implicit admin access on org workspaces, so - * deleting their explicit permission row wouldn't actually revoke access. - * Only regular/external members can be removed from a single workspace. - */ + const lockReason = rowUserIsOrgAdmin ? workspaceRoleLockReason('org-admin') : null const canRemoveFromWorkspace = !rowUserIsOrgAdmin && !isSelf return ( @@ -324,21 +326,25 @@ export function OrganizationMemberLists({ image={member.image} status={`Joined ${formatJoinedDate(member.createdAt)}`} roleControl={ - - updatePermissions - .mutateAsync({ - workspaceId, - organizationId, - updates: [{ userId: member.userId, permissions: permission as PermissionType }], - }) - .catch((error) => logger.error('Failed to update workspace permission', { error })) - } - options={WORKSPACE_ROLE_OPTIONS} - matchTriggerWidth={false} - disabled={disabled} - /> + + + updatePermissions + .mutateAsync({ + workspaceId, + organizationId, + updates: [{ userId: member.userId, permissions: permission as PermissionType }], + }) + .catch((error) => + logger.error('Failed to update workspace permission', { error }) + ) + } + options={WORKSPACE_ROLE_OPTIONS} + matchTriggerWidth={false} + disabled={disabled} + /> + } menu={buildActionsMenu( <> diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx index ea442e0e9de..fcdd11ce842 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx @@ -18,6 +18,11 @@ import { Search, toast, } from '@/components/emcn' +import { + RoleLockTooltip, + type WorkspaceRoleSource, + workspaceRoleLockReason, +} from '@/components/permissions' import type { WorkspacePermission } from '@/lib/api/contracts/workspaces' import { MemberRow, @@ -57,6 +62,7 @@ interface Teammate { userId?: string invitationId?: string token?: string + roleSource?: WorkspaceRoleSource } function formatJoinedDate(iso: string) { @@ -129,6 +135,7 @@ export function Teammates() { status: `Joined ${formatJoinedDate(member.joinedAt)}`, isPending: false, userId: member.userId, + roleSource: member.roleSource, })) const pending: Teammate[] = (invitations ?? []).map((invitation) => ({ @@ -215,17 +222,27 @@ export function Teammates() { email={teammate.email} image={teammate.image} status={teammate.status} - roleControl={ - handleRoleChange(teammate, role as WorkspacePermission)} - options={ROLE_OPTIONS} - matchTriggerWidth={false} - disabled={ - teammate.isPending || !canManage || teammate.userId === viewer?.userId - } - /> - } + roleControl={(() => { + const lockReason = teammate.isPending + ? null + : workspaceRoleLockReason(teammate.roleSource) + return ( + + handleRoleChange(teammate, role as WorkspacePermission)} + options={ROLE_OPTIONS} + matchTriggerWidth={false} + disabled={ + teammate.isPending || + !canManage || + teammate.userId === viewer?.userId || + lockReason !== null + } + /> + + ) + })()} menu={ diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts b/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts index 3cabcc4b9f6..bf9eeafe42f 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts @@ -1,5 +1,6 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { getErrorMessage } from '@sim/utils/errors' import { toast } from '@/components/emcn' import { requestJson } from '@/lib/api/client/request' @@ -103,7 +104,7 @@ export function useUpgradeState(): UpgradeState { }, [subscription.isPaid, subscriptionData?.data?.billingInterval]) const userRole = subscriptionData?.data?.organization?.role ?? 'member' - const isTeamAdmin = ['owner', 'admin'].includes(userRole) + const isTeamAdmin = isOrgAdminRole(userRole) const permissions = getSubscriptionPermissions( { diff --git a/apps/sim/components/permissions/index.ts b/apps/sim/components/permissions/index.ts index d51f10d8805..69aff65d947 100644 --- a/apps/sim/components/permissions/index.ts +++ b/apps/sim/components/permissions/index.ts @@ -4,3 +4,10 @@ export { PermissionSelector, type PermissionType, } from './permission-selector' +export { + type CredentialRoleSource, + credentialRoleLockReason, + RoleLockTooltip, + type WorkspaceRoleSource, + workspaceRoleLockReason, +} from './role-lock' diff --git a/apps/sim/components/permissions/role-lock.tsx b/apps/sim/components/permissions/role-lock.tsx new file mode 100644 index 00000000000..a20029a9ce4 --- /dev/null +++ b/apps/sim/components/permissions/role-lock.tsx @@ -0,0 +1,54 @@ +'use client' + +import type { ReactNode } from 'react' +import { Tooltip } from '@/components/emcn' + +export type WorkspaceRoleSource = 'owner' | 'explicit' | 'org-admin' +export type CredentialRoleSource = 'explicit' | 'workspace-admin' + +/** + * Explanation shown when a workspace member's role is fixed by inheritance and + * cannot be edited. Returns null for editable (`explicit`) roles. + */ +export function workspaceRoleLockReason( + roleSource: WorkspaceRoleSource | undefined +): string | null { + if (roleSource === 'org-admin') return 'Organization admins are automatically workspace admins' + if (roleSource === 'owner') return 'Workspace owner' + return null +} + +/** + * Explanation shown when a credential member's role is fixed because they are a + * workspace admin. Returns null for editable (`explicit`) roles. + */ +export function credentialRoleLockReason( + roleSource: CredentialRoleSource | undefined +): string | null { + if (roleSource === 'workspace-admin') { + return 'Workspace admins are automatically credential admins' + } + return null +} + +interface RoleLockTooltipProps { + reason: string | null + children: ReactNode +} + +/** + * Wraps a disabled role control in a tooltip explaining why the role is fixed. + * Renders children unchanged when there is no lock reason. + */ +export function RoleLockTooltip({ reason, children }: RoleLockTooltipProps) { + if (!reason) return <>{children} + + return ( + + +
{children}
+
+ {reason} +
+ ) +} diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 3bd3c56006d..da5b8b55e2d 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -1163,7 +1163,9 @@ export function AccessControl() { if (!canManage) { return (
- Only organization admins on Enterprise plans can manage Access Control settings. + {!organizationId + ? "Access Control applies to organization workspaces. This workspace isn't part of an organization." + : 'Only organization admins on Enterprise plans can manage Access Control settings.'}
) } diff --git a/apps/sim/ee/data-drains/components/data-drains-settings.tsx b/apps/sim/ee/data-drains/components/data-drains-settings.tsx index 1bcd32084eb..aae623477e6 100644 --- a/apps/sim/ee/data-drains/components/data-drains-settings.tsx +++ b/apps/sim/ee/data-drains/components/data-drains-settings.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { toError } from '@sim/utils/errors' import { ChevronDown, Plus } from 'lucide-react' import { @@ -91,7 +92,7 @@ export function DataDrainsSettings() { const userEmail = session?.user?.email const userRole = getUserRole(activeOrganization, userEmail) - const canManage = userRole === 'owner' || userRole === 'admin' + const canManage = isOrgAdminRole(userRole) const { data: drains, isLoading: drainsLoading, error: drainsError } = useDataDrains(orgId) diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index 5ac649517f8..d0a306d6914 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { toError } from '@sim/utils/errors' import { Chip, ChipSelect, toast } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' @@ -71,7 +72,7 @@ export function DataRetentionSettings() { const userEmail = session?.user?.email const userRole = getUserRole(activeOrganization, userEmail) - const canManage = userRole === 'owner' || userRole === 'admin' + const canManage = isOrgAdminRole(userRole) const [logDays, setLogDays] = useState('') const [softDeleteDays, setSoftDeleteDays] = useState('') diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index c10ccb1e89f..692c6db72f4 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { toError } from '@sim/utils/errors' import { Image as ImageIcon, X } from 'lucide-react' import Image from 'next/image' @@ -127,7 +128,7 @@ export function WhitelabelingSettings() { const userEmail = session?.user?.email const userRole = getUserRole(activeOrganization, userEmail) - const canManage = userRole === 'owner' || userRole === 'admin' + const canManage = isOrgAdminRole(userRole) const subscriptionAccess = getSubscriptionAccessState(subscriptionData?.data) const hasEnterprisePlan = subscriptionAccess.hasUsableEnterpriseAccess diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 798ba423ba4..c9bb5291347 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -972,6 +972,7 @@ export class AgentBlockHandler implements BlockHandler { if (providerId === 'vertex' && providerRequest.vertexCredential) { finalApiKey = await resolveVertexCredential( providerRequest.vertexCredential, + ctx.userId, 'vertex-agent' ) } diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index c338b9c310a..664a80a21ae 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -5,6 +5,21 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock) +vi.mock('@/lib/credentials/access', () => ({ + getCredentialActorContext: vi.fn().mockResolvedValue({ + credential: { + id: 'test-vertex-credential-id', + type: 'oauth', + workspaceId: 'test-workspace', + accountId: 'test-vertex-credential-id', + }, + member: { role: 'admin', status: 'active' }, + hasWorkspaceAccess: true, + canWriteWorkspace: true, + isAdmin: true, + }), +})) + import { BlockType } from '@/executor/constants' import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import type { ExecutionContext } from '@/executor/types' @@ -39,6 +54,7 @@ describe('EvaluatorBlockHandler', () => { mockContext = { workflowId: 'test-workflow-id', + userId: 'test-user', blockStates: new Map(), blockLogs: [], metadata: { duration: 0 }, diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index abb14853ec5..3ab07bc3653 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -43,6 +43,7 @@ export class EvaluatorBlockHandler implements BlockHandler { if (providerId === 'vertex' && evaluatorConfig.vertexCredential) { finalApiKey = await resolveVertexCredential( evaluatorConfig.vertexCredential, + ctx.userId, 'vertex-evaluator' ) } diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 90e326c2785..d5e54239b48 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -5,6 +5,21 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock) +vi.mock('@/lib/credentials/access', () => ({ + getCredentialActorContext: vi.fn().mockResolvedValue({ + credential: { + id: 'test-vertex-credential', + type: 'oauth', + workspaceId: 'test-workspace', + accountId: 'test-vertex-credential-id', + }, + member: { role: 'admin', status: 'active' }, + hasWorkspaceAccess: true, + canWriteWorkspace: true, + isAdmin: true, + }), +})) + import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router' import { BlockType } from '@/executor/constants' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' @@ -65,6 +80,7 @@ describe('RouterBlockHandler', () => { mockContext = { workflowId: 'test-workflow-id', + userId: 'test-user', blockStates: new Map(), blockLogs: [], metadata: { duration: 0 }, @@ -363,6 +379,7 @@ describe('RouterBlockHandler V2', () => { mockContext = { workflowId: 'test-workflow-id', + userId: 'test-user', blockStates: new Map(), blockLogs: [], metadata: { duration: 0 }, diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 1f3c7ba5745..12042918b25 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -84,7 +84,11 @@ export class RouterBlockHandler implements BlockHandler { let finalApiKey: string | undefined = routerConfig.apiKey if (providerId === 'vertex' && routerConfig.vertexCredential) { - finalApiKey = await resolveVertexCredential(routerConfig.vertexCredential, 'vertex-router') + finalApiKey = await resolveVertexCredential( + routerConfig.vertexCredential, + ctx.userId, + 'vertex-router' + ) } const providerRequest: Record = { @@ -214,7 +218,11 @@ export class RouterBlockHandler implements BlockHandler { let finalApiKey: string | undefined = routerConfig.apiKey if (providerId === 'vertex' && routerConfig.vertexCredential) { - finalApiKey = await resolveVertexCredential(routerConfig.vertexCredential, 'vertex-router') + finalApiKey = await resolveVertexCredential( + routerConfig.vertexCredential, + ctx.userId, + 'vertex-router' + ) } const providerRequest: Record = { diff --git a/apps/sim/executor/utils/vertex-credential.ts b/apps/sim/executor/utils/vertex-credential.ts index 15f5d9a6221..b902f29f2fb 100644 --- a/apps/sim/executor/utils/vertex-credential.ts +++ b/apps/sim/executor/utils/vertex-credential.ts @@ -2,48 +2,60 @@ import { db } from '@sim/db' import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { - getServiceAccountToken, - refreshTokenIfNeeded, - resolveOAuthAccountId, -} from '@/app/api/auth/oauth/utils' +import { getCredentialActorContext } from '@/lib/credentials/access' +import { getServiceAccountToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' const logger = createLogger('VertexCredential') /** * Resolves a Vertex AI OAuth credential to an access token. - * Shared across agent, evaluator, and router handlers. + * Shared across agent, evaluator, and router handlers. Authorizes the executing + * user against the credential first — workspace credentials are usable by their + * members and by derived workspace admins, matching `authorizeCredentialUse`. */ export async function resolveVertexCredential( credentialId: string, + actingUserId: string | undefined, callerLabel = 'vertex' ): Promise { const requestId = `${callerLabel}-${Date.now()}` logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) + if (!actingUserId) { + throw new Error('Vertex AI credential use requires an authenticated user') + } + + const access = await getCredentialActorContext(credentialId, actingUserId) + const cred = access.credential + if (!cred) { + throw new Error(`Vertex AI credential not found: ${credentialId}`) + } + if (!access.hasWorkspaceAccess || (!access.member && !access.isAdmin)) { + throw new Error('Not authorized to use this Vertex AI credential') } - if (resolved.credentialType === 'service_account' && resolved.credentialId) { - const accessToken = await getServiceAccountToken(resolved.credentialId, [ + if (cred.type === 'service_account') { + const accessToken = await getServiceAccountToken(cred.id, [ 'https://www.googleapis.com/auth/cloud-platform', ]) logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`) return accessToken } - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), + if (cred.type !== 'oauth' || !cred.accountId) { + throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) + } + + const accountRow = await db.query.account.findFirst({ + where: eq(account.id, cred.accountId), }) - if (!credential) { + if (!accountRow) { throw new Error(`Vertex AI credential not found: ${credentialId}`) } - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) + const { accessToken } = await refreshTokenIfNeeded(requestId, accountRow, cred.accountId) if (!accessToken) { throw new Error('Failed to get Vertex AI access token') diff --git a/apps/sim/hooks/queries/utils/folder-tree.ts b/apps/sim/hooks/queries/utils/folder-tree.ts index dd16ebfcb8f..39add522c57 100644 --- a/apps/sim/hooks/queries/utils/folder-tree.ts +++ b/apps/sim/hooks/queries/utils/folder-tree.ts @@ -82,7 +82,7 @@ export function findLockedAncestorFolder( /** * Effective lock state for a workflow as visible to the client. Mirrors - * the server's `getWorkflowLockStatus(workflowId)` (in `@sim/workflow-authz`) + * the server's `getWorkflowLockStatus(workflowId)` (in `@sim/platform-authz/workflow`) * but reads from cached folder data instead of issuing DB walks. Treats an * undefined workflow as unlocked so callers don't need to early-return. */ @@ -97,7 +97,7 @@ export function isWorkflowEffectivelyLocked( /** * Effective lock state for a folder as visible to the client. Mirrors the - * server's `getFolderLockStatus(folderId)` (in `@sim/workflow-authz`) but + * server's `getFolderLockStatus(folderId)` (in `@sim/platform-authz/workflow`) but * reads from cached folder data instead of issuing DB walks. Treats an * undefined folder as unlocked so callers don't need to early-return. */ diff --git a/apps/sim/lib/api/contracts/credentials.ts b/apps/sim/lib/api/contracts/credentials.ts index 20c46095a31..cec3b73c7e8 100644 --- a/apps/sim/lib/api/contracts/credentials.ts +++ b/apps/sim/lib/api/contracts/credentials.ts @@ -229,6 +229,8 @@ export const workspaceCredentialMemberSchema = z.object({ userName: z.string().nullable(), userEmail: z.string().nullable(), userImage: z.string().nullable().optional(), + /** `workspace-admin` roles are derived from workspace admin and cannot be changed. */ + roleSource: z.enum(['explicit', 'workspace-admin']).optional(), }) export type WorkspaceCredentialMember = z.output diff --git a/apps/sim/lib/api/contracts/organization.ts b/apps/sim/lib/api/contracts/organization.ts index eaa4c3dbf75..1437174e189 100644 --- a/apps/sim/lib/api/contracts/organization.ts +++ b/apps/sim/lib/api/contracts/organization.ts @@ -370,6 +370,8 @@ export const removeOrganizationMemberContract = defineRouteContract({ export const organizationMemberUsageLimitDataSchema = z.object({ creditsUsed: z.number(), creditLimit: z.number().nullable(), + /** Billing cadence of the org's subscription, so the UI can label the usage window. */ + billingInterval: z.enum(['month', 'year']), }) export const getOrganizationMemberUsageLimitContract = defineRouteContract({ diff --git a/apps/sim/lib/api/contracts/workspaces.ts b/apps/sim/lib/api/contracts/workspaces.ts index 361004bdf25..ebee4b66972 100644 --- a/apps/sim/lib/api/contracts/workspaces.ts +++ b/apps/sim/lib/api/contracts/workspaces.ts @@ -84,6 +84,7 @@ export const workspaceUserSchema = z.object({ permissionType: workspacePermissionSchema, isExternal: z.boolean(), joinedAt: z.string(), + roleSource: z.enum(['owner', 'explicit', 'org-admin']), }) export type WorkspaceUser = z.output diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index df2c01f5de4..df12ab0f86f 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -210,13 +210,27 @@ export const auth = betterAuth({ ) } - const { reassignBilledAccountForUser } = await import('@/lib/workspaces/utils') + const { reassignBilledAccountForUser, reassignOwnedWorkspacesForUser } = await import( + '@/lib/workspaces/utils' + ) const { unresolved } = await reassignBilledAccountForUser(deletingUser.id) if (unresolved.length > 0) { throw new Error( `Your account is the billing account for ${unresolved.length} workspace${unresolved.length === 1 ? '' : 's'} with no other admin to take it over. Add another admin to ${unresolved.length === 1 ? 'that workspace' : 'those workspaces'} or delete ${unresolved.length === 1 ? 'it' : 'them'} before deleting your account.` ) } + + // Reassign workspace ownership BEFORE deletion so the `workspace.owner_id` + // ON DELETE CASCADE can never silently nuke workspaces this user owns + // (e.g. org workspaces they created but are billed to the org owner). + const { unresolved: ownedUnresolved } = await reassignOwnedWorkspacesForUser( + deletingUser.id + ) + if (ownedUnresolved.length > 0) { + throw new Error( + `Your account owns ${ownedUnresolved.length} workspace${ownedUnresolved.length === 1 ? '' : 's'} with no other admin to take over ownership. Add another admin to ${ownedUnresolved.length === 1 ? 'that workspace' : 'those workspaces'} or delete ${ownedUnresolved.length === 1 ? 'it' : 'them'} before deleting your account.` + ) + } }, }, }, diff --git a/apps/sim/lib/auth/credential-access.ts b/apps/sim/lib/auth/credential-access.ts index 97a9bf50b15..5dcddf5d45f 100644 --- a/apps/sim/lib/auth/credential-access.ts +++ b/apps/sim/lib/auth/credential-access.ts @@ -97,16 +97,16 @@ export async function authorizeCredentialUse( ) .limit(1) - if (!membership) { + if (requesterPerm === null) { + return { ok: false, error: 'You do not have access to this workspace.' } + } + if (!membership && requesterPerm !== 'admin') { return { ok: false, error: 'You do not have access to this credential. Ask the credential admin to add you as a member.', } } - if (requesterPerm === null) { - return { ok: false, error: 'You do not have access to this workspace.' } - } return { ok: true, @@ -155,16 +155,16 @@ export async function authorizeCredentialUse( ) .limit(1) - if (!membership) { + if (requesterPerm === null) { return { ok: false, - error: `You do not have access to this credential. Ask the credential admin to add you as a member.`, + error: 'You do not have access to this workspace.', } } - if (requesterPerm === null) { + if (!membership && requesterPerm !== 'admin') { return { ok: false, - error: 'You do not have access to this workspace.', + error: `You do not have access to this credential. Ask the credential admin to add you as a member.`, } } @@ -232,10 +232,17 @@ export async function authorizeCredentialUse( .limit(1) if (!membership) { - return { - ok: false, - error: - 'You do not have access to this credential. Ask the credential admin to add you as a member.', + const requesterPerm = await getUserEntityPermissions( + actingUserId, + 'workspace', + workflowContext.workspaceId + ) + if (requesterPerm !== 'admin') { + return { + ok: false, + error: + 'You do not have access to this credential. Ask the credential admin to add you as a member.', + } } } diff --git a/apps/sim/lib/billing/client/upgrade.ts b/apps/sim/lib/billing/client/upgrade.ts index dc6e8e09691..2c5d3dad651 100644 --- a/apps/sim/lib/billing/client/upgrade.ts +++ b/apps/sim/lib/billing/client/upgrade.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { getErrorMessage } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' import { ApiClientError } from '@/lib/api/client/errors' @@ -80,9 +81,7 @@ export function useSubscriptionUpgrade() { } throw err } - const existingOrg = orgsData.organizations?.find( - (org) => org.role === 'owner' || org.role === 'admin' - ) + const existingOrg = orgsData.organizations?.find((org) => isOrgAdminRole(org.role)) if (existingOrg) { const existingOrgSub = allSubscriptions.find( diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index f1b3ae26369..67d3170389f 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -2,9 +2,8 @@ import { db } from '@sim/db' import { member, organization, subscription, userStats } from '@sim/db/schema' import { and, desc, eq, inArray } from 'drizzle-orm' import { - getBillingInterval, getHighestPrioritySubscription, - type SubscriptionMetadata, + resolveBillingInterval, } from '@/lib/billing/core/subscription' import { getOrgUsageLimit, getUserUsageData } from '@/lib/billing/core/usage' import { COPILOT_USAGE_SOURCES, getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' @@ -544,7 +543,7 @@ export async function getSimplifiedBillingSummary( : 0 const orgCredits = await getCreditBalance(userId, executor) - const orgBillingInterval = getBillingInterval(subscription.metadata as SubscriptionMetadata) + const orgBillingInterval = resolveBillingInterval(subscription) return { type: 'organization', @@ -643,9 +642,7 @@ export async function getSimplifiedBillingSummary( : 0 const userCredits = await getCreditBalance(userId, executor) - const individualBillingInterval = getBillingInterval( - subscription?.metadata as SubscriptionMetadata - ) + const individualBillingInterval = resolveBillingInterval(subscription) return { type: 'individual', diff --git a/apps/sim/lib/billing/core/organization.ts b/apps/sim/lib/billing/core/organization.ts index 2000edd898c..ca09e106c3d 100644 --- a/apps/sim/lib/billing/core/organization.ts +++ b/apps/sim/lib/billing/core/organization.ts @@ -20,6 +20,7 @@ import { } from '@/lib/billing/subscriptions/utils' import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' import type { DbClient } from '@/lib/db/types' +import { isOrganizationAdminOrOwner } from '@/lib/workspaces/permissions/utils' const logger = createLogger('OrganizationBilling') @@ -419,7 +420,11 @@ async function getOrganizationBillingSummary(organizationId: string) { } /** - * Check if a user is an owner or admin of a specific organization + * Error-tolerant wrapper around {@link isOrganizationAdminOrOwner} for billing + * gates: on a DB error it logs and returns false instead of throwing, so a + * transient failure denies access rather than surfacing a 500 mid-checkout. + * Prefer the canonical {@link isOrganizationAdminOrOwner} when a thrown error + * should propagate. * * @param userId - The ID of the user to check * @param organizationId - The ID of the organization @@ -430,18 +435,7 @@ export async function isOrganizationOwnerOrAdmin( organizationId: string ): Promise { try { - const memberRecord = await db - .select({ role: member.role }) - .from(member) - .where(and(eq(member.userId, userId), eq(member.organizationId, organizationId))) - .limit(1) - - if (memberRecord.length === 0) { - return false - } - - const userRole = memberRecord[0].role - return ['owner', 'admin'].includes(userRole) + return await isOrganizationAdminOrOwner(userId, organizationId) } catch (error) { logger.error('Error checking organization ownership/admin status:', error) return false diff --git a/apps/sim/lib/billing/core/subscription.ts b/apps/sim/lib/billing/core/subscription.ts index ca0996eabc6..6829a773e0a 100644 --- a/apps/sim/lib/billing/core/subscription.ts +++ b/apps/sim/lib/billing/core/subscription.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { member, organization, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, inArray, sql } from 'drizzle-orm' import { getEffectiveBillingStatus, isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { @@ -52,6 +53,21 @@ export function getBillingInterval( return metadata?.billingInterval === 'year' ? 'year' : 'month' } +/** + * Resolves a subscription's effective billing interval. Prefers the Stripe-synced + * `billingInterval` column — the only source populated on enterprise/manual + * subscriptions, which skip the checkout flow that writes the metadata value — and + * falls back to `metadata.billingInterval` (the column is often null on + * checkout-created subs), defaulting to monthly. Where both are set they agree. + */ +export function resolveBillingInterval( + sub: { billingInterval?: string | null; metadata?: unknown } | null | undefined +): 'month' | 'year' { + const column = sub?.billingInterval + if (column === 'year' || column === 'month') return column + return getBillingInterval((sub?.metadata ?? null) as SubscriptionMetadata | null) +} + /** * Merge a `billingInterval` value into a subscription's metadata JSON column. */ @@ -183,7 +199,7 @@ export async function getOrganizationIdForSubscriptionReference( .where(eq(member.userId, referenceId)) .limit(1) - if (memberRecord && (memberRecord.role === 'owner' || memberRecord.role === 'admin')) { + if (memberRecord && isOrgAdminRole(memberRecord.role)) { return memberRecord.organizationId } diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index ef3cc7d7631..baa893f451d 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { member, organization, settings, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import { @@ -891,7 +892,7 @@ export async function maybeSendUsageThresholdEmail(params: { .where(eq(member.organizationId, params.organizationId)) for (const a of admins) { - const isAdmin = a.role === 'owner' || a.role === 'admin' + const isAdmin = isOrgAdminRole(a.role) if (!isAdmin) continue if (a.enabled === false) continue if (!a.email) continue diff --git a/apps/sim/lib/billing/organization.ts b/apps/sim/lib/billing/organization.ts index c1df4aefdc3..a288a0239e2 100644 --- a/apps/sim/lib/billing/organization.ts +++ b/apps/sim/lib/billing/organization.ts @@ -7,6 +7,7 @@ import { userStats, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, inArray, sql } from 'drizzle-orm' import { getPlanPricing } from '@/lib/billing/core/billing' import { getOrganizationIdForSubscriptionReference } from '@/lib/billing/core/subscription' @@ -133,7 +134,7 @@ export async function ensureOrganizationForTeamSubscription( if (existingMembership.length > 0) { const membership = existingMembership[0] - if (membership.role === 'owner' || membership.role === 'admin') { + if (isOrgAdminRole(membership.role)) { /** * Atomic duplicate-subscription check + referenceId transfer. * diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index 239423a4684..6c15b325677 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -7,8 +7,6 @@ import { db } from '@sim/db' import { - credential, - credentialMember, invitation, member, organization, @@ -30,6 +28,7 @@ import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' import { OUTBOX_EVENT_TYPES } from '@/lib/billing/webhooks/outbox-handlers' import { enqueueOutboxEvent } from '@/lib/core/outbox/service' +import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' import type { DbOrTx } from '@/lib/db/types' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, @@ -400,109 +399,6 @@ async function reassignOwnedOrganizationWorkspacesTx({ return reassignedWorkspaces.length } -async function revokeWorkspaceCredentialMembershipsTx({ - tx, - workspaceIds, - userId, -}: { - tx: DbOrTx - workspaceIds: string[] - userId: string -}) { - if (workspaceIds.length === 0) return 0 - - const workspaceCredentialRows = await tx - .select({ - credentialId: credential.id, - workspaceId: credential.workspaceId, - ownerId: workspace.ownerId, - }) - .from(credential) - .innerJoin(workspace, eq(credential.workspaceId, workspace.id)) - .where(inArray(credential.workspaceId, workspaceIds)) - - if (workspaceCredentialRows.length === 0) return 0 - - const credentialIds = workspaceCredentialRows.map((row) => row.credentialId) - const ownerByCredentialId = new Map( - workspaceCredentialRows.map((row) => [row.credentialId, row.ownerId]) - ) - - const userAdminMemberships = await tx - .select({ credentialId: credentialMember.credentialId }) - .from(credentialMember) - .where( - and( - eq(credentialMember.userId, userId), - eq(credentialMember.role, 'admin'), - eq(credentialMember.status, 'active'), - inArray(credentialMember.credentialId, credentialIds) - ) - ) - - for (const { credentialId } of userAdminMemberships) { - const ownerId = ownerByCredentialId.get(credentialId) - if (!ownerId || ownerId === userId) continue - - const otherAdmins = await tx - .select({ id: credentialMember.id }) - .from(credentialMember) - .where( - and( - eq(credentialMember.credentialId, credentialId), - eq(credentialMember.role, 'admin'), - eq(credentialMember.status, 'active'), - ne(credentialMember.userId, userId) - ) - ) - .limit(1) - - if (otherAdmins.length > 0) continue - - const now = new Date() - const [existingOwnerMembership] = await tx - .select({ id: credentialMember.id }) - .from(credentialMember) - .where( - and(eq(credentialMember.credentialId, credentialId), eq(credentialMember.userId, ownerId)) - ) - .limit(1) - - if (existingOwnerMembership) { - await tx - .update(credentialMember) - .set({ role: 'admin', status: 'active', updatedAt: now }) - .where(eq(credentialMember.id, existingOwnerMembership.id)) - } else { - await tx.insert(credentialMember).values({ - id: generateId(), - credentialId, - userId: ownerId, - role: 'admin', - status: 'active', - joinedAt: now, - invitedBy: ownerId, - createdAt: now, - updatedAt: now, - }) - } - } - - const revokedMemberships = await tx - .update(credentialMember) - .set({ status: 'revoked', updatedAt: new Date() }) - .where( - and( - eq(credentialMember.userId, userId), - eq(credentialMember.status, 'active'), - inArray(credentialMember.credentialId, credentialIds) - ) - ) - .returning({ credentialId: credentialMember.credentialId }) - - return revokedMemberships.length -} - interface MembershipValidationResult { canAdd: boolean reason?: string @@ -1114,11 +1010,11 @@ export async function removeUserFromOrganization( ) .returning({ entityId: permissions.entityId }) - const credentialMembershipsRevoked = await revokeWorkspaceCredentialMembershipsTx({ + const credentialMembershipsRevoked = await revokeWorkspaceCredentialMembershipsTx( tx, workspaceIds, - userId, - }) + userId + ) const capturedUsage = await captureDepartedUsage() return { @@ -1308,11 +1204,11 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { ) .returning({ id: permissionGroupMember.id }) - const credentialMembershipsRevoked = await revokeWorkspaceCredentialMembershipsTx({ + const credentialMembershipsRevoked = await revokeWorkspaceCredentialMembershipsTx( tx, workspaceIds, - userId, - }) + userId + ) const cancelledInvitations = targetUser?.email ? await tx diff --git a/apps/sim/lib/billing/webhooks/invoices.ts b/apps/sim/lib/billing/webhooks/invoices.ts index ff7c2294df4..a3e026f2006 100644 --- a/apps/sim/lib/billing/webhooks/invoices.ts +++ b/apps/sim/lib/billing/webhooks/invoices.ts @@ -8,6 +8,7 @@ import { userStats, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, inArray, isNull, ne, or, sql } from 'drizzle-orm' import type Stripe from 'stripe' import { getEmailSubject, PaymentFailedEmail, renderCreditPurchaseEmail } from '@/components/emails' @@ -300,9 +301,7 @@ async function sendPaymentFailureEmails( .from(member) .where(eq(member.organizationId, sub.referenceId)) - const ownerAdminIds = members - .filter((m) => m.role === 'owner' || m.role === 'admin') - .map((m) => m.userId) + const ownerAdminIds = members.filter((m) => isOrgAdminRole(m.role)).map((m) => m.userId) if (ownerAdminIds.length > 0) { const users = await db @@ -722,9 +721,7 @@ async function handleCreditPurchaseSuccess(invoice: Stripe.Invoice): Promise m.role === 'owner' || m.role === 'admin') - .map((m) => m.userId) + const ownerAdminIds = members.filter((m) => isOrgAdminRole(m.role)).map((m) => m.userId) if (ownerAdminIds.length > 0) { recipients = await db diff --git a/apps/sim/lib/copilot/auth/permissions.test.ts b/apps/sim/lib/copilot/auth/permissions.test.ts index 9e0c4dcb3e8..93a62fc3171 100644 --- a/apps/sim/lib/copilot/auth/permissions.test.ts +++ b/apps/sim/lib/copilot/auth/permissions.test.ts @@ -1,102 +1,81 @@ /** * @vitest-environment node */ -import { permissionsMock, permissionsMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetActiveWorkflowContext } = vi.hoisted(() => ({ - mockGetActiveWorkflowContext: vi.fn(), +const { mockAuthorizeWorkflowByWorkspacePermission } = vi.hoisted(() => ({ + mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), })) -const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions - -vi.mock('@sim/workflow-authz', () => ({ - getActiveWorkflowContext: mockGetActiveWorkflowContext, +vi.mock('@sim/platform-authz/workflow', () => ({ + authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission, })) -vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) - import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' describe('Copilot Auth Permissions', () => { beforeEach(() => { vi.clearAllMocks() - - mockGetActiveWorkflowContext.mockResolvedValue(null) }) describe('verifyWorkflowAccess', () => { it('should return no access for non-existent workflow', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce(null) - - const result = await verifyWorkflowAccess('user-123', 'non-existent-workflow') - - expect(result).toEqual({ - hasAccess: false, - userPermission: null, + mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + allowed: false, + status: 404, + workflow: null, + workspacePermission: null, }) - }) - it('should check workspace permissions for workflow with workspace', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce({ - workflow: {}, - workspaceId: 'workspace-456', - }) - mockGetUserEntityPermissions.mockResolvedValueOnce('write') - - const result = await verifyWorkflowAccess('user-123', 'workflow-789') - - expect(result).toEqual({ - hasAccess: true, - userPermission: 'write', - workspaceId: 'workspace-456', - }) - - expect(mockGetUserEntityPermissions).toHaveBeenCalledWith( - 'user-123', - 'workspace', - 'workspace-456' - ) - }) - - it('should return read permission through workspace', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce({ - workflow: {}, - workspaceId: 'workspace-456', - }) - mockGetUserEntityPermissions.mockResolvedValueOnce('read') - - const result = await verifyWorkflowAccess('user-123', 'workflow-789') + const result = await verifyWorkflowAccess('user-123', 'non-existent-workflow') - expect(result).toEqual({ - hasAccess: true, - userPermission: 'read', - workspaceId: 'workspace-456', - }) + expect(result).toEqual({ hasAccess: false, userPermission: null }) }) - it('should return admin permission through workspace', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce({ - workflow: {}, - workspaceId: 'workspace-456', + it('should delegate to the shared workflow authorizer with a read action', async () => { + mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + allowed: true, + status: 200, + workflow: { workspaceId: 'workspace-456' }, + workspacePermission: 'write', }) - mockGetUserEntityPermissions.mockResolvedValueOnce('admin') - const result = await verifyWorkflowAccess('user-123', 'workflow-789') + await verifyWorkflowAccess('user-123', 'workflow-789') - expect(result).toEqual({ - hasAccess: true, - userPermission: 'admin', - workspaceId: 'workspace-456', + expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith({ + workflowId: 'workflow-789', + userId: 'user-123', + action: 'read', }) }) - it('should return no access without workspace permissions', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce({ - workflow: {}, - workspaceId: 'workspace-456', + it.each(['read', 'write', 'admin'] as const)( + 'should grant access with %s permission through the workspace', + async (permission) => { + mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + allowed: true, + status: 200, + workflow: { workspaceId: 'workspace-456' }, + workspacePermission: permission, + }) + + const result = await verifyWorkflowAccess('user-123', 'workflow-789') + + expect(result).toEqual({ + hasAccess: true, + userPermission: permission, + workspaceId: 'workspace-456', + }) + } + ) + + it('should report the workspaceId even when permission is denied for an existing workflow', async () => { + mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + allowed: false, + status: 403, + workflow: { workspaceId: 'workspace-456' }, + workspacePermission: null, }) - mockGetUserEntityPermissions.mockResolvedValueOnce(null) const result = await verifyWorkflowAccess('user-123', 'workflow-789') @@ -107,41 +86,27 @@ describe('Copilot Auth Permissions', () => { }) }) - it('should return no access for workflow without workspace', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce(null) - - const result = await verifyWorkflowAccess('user-123', 'workflow-789') - - expect(result).toEqual({ - hasAccess: false, - userPermission: null, + it('should return no access for a workflow without a workspace', async () => { + mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + allowed: false, + status: 403, + workflow: { workspaceId: null }, + workspacePermission: null, }) - }) - - it('should handle database errors gracefully', async () => { - mockGetActiveWorkflowContext.mockRejectedValueOnce(new Error('Database connection failed')) const result = await verifyWorkflowAccess('user-123', 'workflow-789') - expect(result).toEqual({ - hasAccess: false, - userPermission: null, - }) + expect(result).toEqual({ hasAccess: false, userPermission: null }) }) - it('should handle permission check errors gracefully', async () => { - mockGetActiveWorkflowContext.mockResolvedValueOnce({ - workflow: {}, - workspaceId: 'workspace-456', - }) - mockGetUserEntityPermissions.mockRejectedValueOnce(new Error('Permission check failed')) + it('should handle errors gracefully', async () => { + mockAuthorizeWorkflowByWorkspacePermission.mockRejectedValueOnce( + new Error('Database connection failed') + ) const result = await verifyWorkflowAccess('user-123', 'workflow-789') - expect(result).toEqual({ - hasAccess: false, - userPermission: null, - }) + expect(result).toEqual({ hasAccess: false, userPermission: null }) }) }) diff --git a/apps/sim/lib/copilot/auth/permissions.ts b/apps/sim/lib/copilot/auth/permissions.ts index ab36213b8ca..31d6972f158 100644 --- a/apps/sim/lib/copilot/auth/permissions.ts +++ b/apps/sim/lib/copilot/auth/permissions.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' -import { getActiveWorkflowContext } from '@sim/workflow-authz' -import { getUserEntityPermissions, type PermissionType } from '@/lib/workspaces/permissions/utils' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' +import type { PermissionType } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CopilotPermissions') @@ -20,42 +20,15 @@ export async function verifyWorkflowAccess( workspaceId?: string }> { try { - const workflowContext = await getActiveWorkflowContext(workflowId) - if (!workflowContext) { - logger.warn('Attempt to access non-existent workflow', { - workflowId, - userId, - }) - return { hasAccess: false, userPermission: null } - } - - const { workspaceId } = workflowContext - - const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - - if (userPermission !== null) { - logger.debug('User has workspace permission for workflow', { - workflowId, - userId, - workspaceId, - userPermission, - }) - return { - hasAccess: true, - userPermission, - workspaceId, - } - } - - logger.warn('User has no access to workflow', { + const result = await authorizeWorkflowByWorkspacePermission({ workflowId, userId, - workspaceId, + action: 'read', }) return { - hasAccess: false, - userPermission: null, - workspaceId: workspaceId || undefined, + hasAccess: result.allowed, + userPermission: result.workspacePermission, + workspaceId: result.workflow?.workspaceId ?? undefined, } } catch (error) { logger.error('Error verifying workflow access', { error, workflowId, userId }) diff --git a/apps/sim/lib/copilot/chat/lifecycle.test.ts b/apps/sim/lib/copilot/chat/lifecycle.test.ts index 8c0e4fe76ef..38dbe31de34 100644 --- a/apps/sim/lib/copilot/chat/lifecycle.test.ts +++ b/apps/sim/lib/copilot/chat/lifecycle.test.ts @@ -11,7 +11,7 @@ const { mockAuthorizeWorkflow, mockGetActiveWorkflow } = vi.hoisted(() => ({ mockGetActiveWorkflow: vi.fn(), })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflow, getActiveWorkflowRecord: mockGetActiveWorkflow, })) diff --git a/apps/sim/lib/copilot/chat/lifecycle.ts b/apps/sim/lib/copilot/chat/lifecycle.ts index 7bca16a8298..7530f3c5ed7 100644 --- a/apps/sim/lib/copilot/chat/lifecycle.ts +++ b/apps/sim/lib/copilot/chat/lifecycle.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission, getActiveWorkflowRecord, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' import { and, asc, eq, isNull, sql } from 'drizzle-orm' import { type PersistedMessage, stripToolResultOutput } from '@/lib/copilot/chat/persisted-message' import { diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index e1347d05316..1778f80c780 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -1,10 +1,10 @@ import { type Context as OtelContext, context as otelContextApi } from '@opentelemetry/api' import { db } from '@sim/db' -import { copilotChats, permissions } from '@sim/db/schema' +import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { isZodError, validationErrorResponse } from '@/lib/api/server' @@ -48,6 +48,7 @@ import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' import { getUserEntityPermissions, isWorkspaceAccessDeniedError, + type PermissionType, } from '@/lib/workspaces/permissions/utils' import type { ChatContext } from '@/stores/panel' @@ -194,6 +195,7 @@ type UnifiedChatBranch = | { kind: 'workspace' workspaceId: string + workspacePermission: PermissionType | null effectiveModel: string goRoute: '/api/mothership' titleModel: string @@ -639,25 +641,20 @@ async function resolveBranch(params: { return createBadRequestResponse('workspaceId is required when workflowId is not provided') } - const [permissionRow] = await db - .select({ permissionType: permissions.permissionType }) - .from(permissions) - .where( - and( - eq(permissions.userId, authenticatedUserId), - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, requestedWorkspaceId) - ) - ) - .limit(1) + const workspacePermission = await getUserEntityPermissions( + authenticatedUserId, + 'workspace', + requestedWorkspaceId + ) - if (!permissionRow) { + if (workspacePermission === null) { return createBadRequestResponse('Workspace not found or access denied') } return { kind: 'workspace', workspaceId: requestedWorkspaceId, + workspacePermission, effectiveModel: DEFAULT_MODEL, goRoute: '/api/mothership', titleModel: DEFAULT_MODEL, @@ -880,15 +877,22 @@ export async function handleUnifiedChatPost(req: NextRequest) { }) const workspaceId = branch.workspaceId - const userPermissionPromise = workspaceId - ? getUserEntityPermissions(authenticatedUserId, 'workspace', workspaceId).catch((error) => { - logger.warn('Failed to load user permissions', { - error: getErrorMessage(error), - workspaceId, - }) - return null - }) - : Promise.resolve(null) + // The workspace branch already resolved this permission (and gated on it) + // during branch resolution; reuse it instead of querying again. + const userPermissionPromise = + branch.kind === 'workspace' + ? Promise.resolve(branch.workspacePermission) + : workspaceId + ? getUserEntityPermissions(authenticatedUserId, 'workspace', workspaceId).catch( + (error) => { + logger.warn('Failed to load user permissions', { + error: getErrorMessage(error), + workspaceId, + }) + return null + } + ) + : Promise.resolve(null) // Wrap the pre-LLM prep work in spans so the trace waterfall shows // where time is going between "request received" and "llm.stream // opens". Previously these ran bare under the root and inflated the diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 3edafe1ca93..ef33580211c 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission, getActiveWorkflowRecord, -} from '@sim/workflow-authz' +} from '@sim/platform-authz/workflow' import { and, eq, isNull, ne } from 'drizzle-orm' import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' import { diff --git a/apps/sim/lib/copilot/tools/handlers/access.ts b/apps/sim/lib/copilot/tools/handlers/access.ts index a435511a2f8..7391a829ead 100644 --- a/apps/sim/lib/copilot/tools/handlers/access.ts +++ b/apps/sim/lib/copilot/tools/handlers/access.ts @@ -1,9 +1,7 @@ -import { db } from '@sim/db' -import { permissions, workspace } from '@sim/db/schema' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' -import { and, desc, eq, isNull } from 'drizzle-orm' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import type { getWorkflowById } from '@/lib/workflows/utils' -import { checkWorkspaceAccess, getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' type WorkflowRecord = NonNullable>> @@ -33,26 +31,16 @@ export async function ensureWorkflowAccess( } export async function getDefaultWorkspaceId(userId: string): Promise { - const workspaces = await db - .select({ workspaceId: workspace.id }) - .from(permissions) - .innerJoin(workspace, eq(permissions.entityId, workspace.id)) - .where( - and( - eq(permissions.userId, userId), - eq(permissions.entityType, 'workspace'), - isNull(workspace.archivedAt) - ) - ) - .orderBy(desc(workspace.createdAt)) - .limit(1) + const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId) + const mostRecent = accessibleRows + .map((row) => row.workspace) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0] - const workspaceId = workspaces[0]?.workspaceId - if (!workspaceId) { + if (!mostRecent) { throw new Error('No workspace found for user') } - return workspaceId + return mostRecent.id } export async function ensureWorkspaceAccess( @@ -68,9 +56,7 @@ export async function ensureWorkspaceAccess( if (level === 'read') return if (level === 'admin') { - if (access.workspace?.ownerId === userId) return - const perm = await getUserEntityPermissions(userId, 'workspace', workspaceId) - if (perm !== 'admin') { + if (!access.canAdmin) { throw new Error('Admin access required for this workspace') } return diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index e72abc1082d..2e0f7fc5300 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -1,9 +1,9 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db, workflow as workflowTable } from '@sim/db' import { createLogger } from '@sim/logger' +import { assertFolderMutable, assertWorkflowMutable } from '@sim/platform-authz/workflow' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { assertFolderMutable, assertWorkflowMutable } from '@sim/workflow-authz' import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' import { eq } from 'drizzle-orm' import { performCreateWorkspaceApiKey } from '@/lib/api-key/orchestration' diff --git a/apps/sim/lib/copilot/tools/server/user/get-credentials.ts b/apps/sim/lib/copilot/tools/server/user/get-credentials.ts index 247455e923c..b0d9221c9f2 100644 --- a/apps/sim/lib/copilot/tools/server/user/get-credentials.ts +++ b/apps/sim/lib/copilot/tools/server/user/get-credentials.ts @@ -6,8 +6,10 @@ import { eq } from 'drizzle-orm' import { decodeJwt } from 'jose' import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' import { getAllOAuthServices } from '@/lib/oauth' +import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' interface GetCredentialsParams { workflowId?: string @@ -47,6 +49,12 @@ export const getCredentialsServerTool: BaseServerTool const userId = authenticatedUserId + // Resolve workspace access once and thread it into both credential lookups + // below; each would otherwise re-resolve the same workspace-admin status. + const workspaceAccess: WorkspaceAccess | undefined = workspaceId + ? await checkWorkspaceAccess(workspaceId, userId) + : undefined + logger.info('Fetching credentials for authenticated user', { userId, hasWorkflowId: !!params?.workflowId, @@ -110,6 +118,31 @@ export const getCredentialsServerTool: BaseServerTool }) } + // Surface workspace-shared OAuth/service-account credentials the user can use, + // including those they reach as a derived workspace admin (not just their own + // personal account connections). Keyed by credential id so the agent references + // the workspace credential, not a legacy account id. + if (workspaceId) { + const sharedCredentials = await getAccessibleOAuthCredentials(workspaceId, userId, { + isWorkspaceAdmin: workspaceAccess?.canAdmin ?? false, + }) + const seenCredentialIds = new Set(connectedCredentials.map((c) => c.id)) + for (const cred of sharedCredentials) { + if (seenCredentialIds.has(cred.id)) continue + connectedProviderIds.add(cred.providerId) + const [, featureType = 'default'] = cred.providerId.split('-') + connectedCredentials.push({ + id: cred.id, + name: cred.displayName, + provider: cred.providerId, + serviceName: + allOAuthServices.find((s) => s.providerId === cred.providerId)?.name ?? cred.providerId, + lastUsed: cred.updatedAt.toISOString(), + isDefault: featureType === 'default', + }) + } + } + // Build list of not connected services const notConnectedServices = allOAuthServices .filter((service) => !connectedProviderIds.has(service.providerId)) @@ -121,7 +154,11 @@ export const getCredentialsServerTool: BaseServerTool })) // Fetch environment variables from both personal and workspace - const envResult = await getPersonalAndWorkspaceEnv(userId, workspaceId) + const envResult = await getPersonalAndWorkspaceEnv( + userId, + workspaceId, + workspaceAccess ? { workspaceAccess } : undefined + ) // Get all unique variable names from both personal and workspace const personalVarNames = Object.keys(envResult.personalEncrypted) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts index 96a4fa98353..118060afd55 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts @@ -1,8 +1,11 @@ import { db } from '@sim/db' import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { + assertWorkflowMutable, + authorizeWorkflowByWorkspacePermission, +} from '@sim/platform-authz/workflow' import { toError } from '@sim/utils/errors' -import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { EditWorkflow } from '@/lib/copilot/generated/tool-catalog-v1' import { diff --git a/apps/sim/lib/copilot/validation/selector-validator.test.ts b/apps/sim/lib/copilot/validation/selector-validator.test.ts index 13df3491273..7d068c03b20 100644 --- a/apps/sim/lib/copilot/validation/selector-validator.test.ts +++ b/apps/sim/lib/copilot/validation/selector-validator.test.ts @@ -4,12 +4,21 @@ import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +const { mockCheckWorkspaceAccess } = vi.hoisted(() => ({ + mockCheckWorkspaceAccess: vi.fn(), +})) + vi.mock('@sim/db', () => dbChainMock) +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + checkWorkspaceAccess: mockCheckWorkspaceAccess, +})) + vi.mock('drizzle-orm', () => ({ and: vi.fn((...args: unknown[]) => ({ type: 'and', args })), eq: vi.fn((...args: unknown[]) => ({ type: 'eq', args })), inArray: vi.fn((...args: unknown[]) => ({ type: 'inArray', args })), + isNotNull: vi.fn((field: unknown) => ({ type: 'isNotNull', field })), isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })), or: vi.fn((...args: unknown[]) => ({ type: 'or', args })), })) @@ -20,6 +29,7 @@ describe('validateSelectorIds', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() + mockCheckWorkspaceAccess.mockResolvedValue({ canAdmin: false }) }) it('accepts shared workspace credential ids and legacy account ids for oauth-input', async () => { @@ -58,4 +68,17 @@ describe('validateSelectorIds', () => { expect(result.warning).toContain('Accessible workspace credentials:') expect(result.warning).toContain('Shared Gmail [cred-2]') }) + + it('lets a derived workspace admin reference shared credentials without membership', async () => { + mockCheckWorkspaceAccess.mockResolvedValueOnce({ canAdmin: true }) + dbChainMockFns.where.mockResolvedValueOnce([{ credentialId: 'shared-cred', accountId: null }]) + + const result = await validateSelectorIds('oauth-input', ['shared-cred'], { + userId: 'admin-user', + workspaceId: 'workspace-1', + }) + + expect(result).toEqual({ valid: ['shared-cred'], invalid: [] }) + expect(dbChainMockFns.select).toHaveBeenCalledTimes(1) + }) }) diff --git a/apps/sim/lib/copilot/validation/selector-validator.ts b/apps/sim/lib/copilot/validation/selector-validator.ts index 7c6fefa6a44..30490f583cc 100644 --- a/apps/sim/lib/copilot/validation/selector-validator.ts +++ b/apps/sim/lib/copilot/validation/selector-validator.ts @@ -10,7 +10,8 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { and, eq, inArray, isNull, or } from 'drizzle-orm' +import { and, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('SelectorValidator') @@ -44,12 +45,21 @@ export async function validateSelectorIds( case 'oauth-input': { if (context.workspaceId) { // In workspace workflows, oauth-input values are workspace credential IDs. - // Accept both current credential IDs and legacy account IDs when the user - // has active membership to the workspace credential. + // Accept both current credential IDs and legacy account IDs. Workspace + // admins (incl. derived org admins) can reference any shared credential; + // other members need active credential membership. + const isWorkspaceAdmin = (await checkWorkspaceAccess(context.workspaceId, context.userId)) + .canAdmin + + const matchWhere = and( + eq(credential.workspaceId, context.workspaceId), + inArray(credential.type, ['oauth', 'service_account']), + or(inArray(credential.id, idsArray), inArray(credential.accountId, idsArray)) + ) const results = await db .select({ credentialId: credential.id, accountId: credential.accountId }) .from(credential) - .innerJoin( + .leftJoin( credentialMember, and( eq(credentialMember.credentialId, credential.id), @@ -57,13 +67,7 @@ export async function validateSelectorIds( eq(credentialMember.status, 'active') ) ) - .where( - and( - eq(credential.workspaceId, context.workspaceId), - inArray(credential.type, ['oauth', 'service_account']), - or(inArray(credential.id, idsArray), inArray(credential.accountId, idsArray)) - ) - ) + .where(and(matchWhere, isWorkspaceAdmin ? undefined : isNotNull(credentialMember.id))) existingIds = Array.from( new Set( @@ -83,17 +87,22 @@ export async function validateSelectorIds( const existingSet = new Set(existingIds) const invalidIds = idsArray.filter((id) => !existingSet.has(id)) if (invalidIds.length > 0) { + const accessibleSelect = { + id: credential.id, + displayName: credential.displayName, + accountId: credential.accountId, + credentialProviderId: credential.providerId, + accountProviderId: account.providerId, + } + const accessibleWhere = and( + eq(credential.workspaceId, context.workspaceId), + inArray(credential.type, ['oauth', 'service_account']) + ) const allAccessibleCredentials = await db - .select({ - id: credential.id, - displayName: credential.displayName, - accountId: credential.accountId, - credentialProviderId: credential.providerId, - accountProviderId: account.providerId, - }) + .select(accessibleSelect) .from(credential) .leftJoin(account, eq(credential.accountId, account.id)) - .innerJoin( + .leftJoin( credentialMember, and( eq(credentialMember.credentialId, credential.id), @@ -102,10 +111,7 @@ export async function validateSelectorIds( ) ) .where( - and( - eq(credential.workspaceId, context.workspaceId), - inArray(credential.type, ['oauth', 'service_account']) - ) + and(accessibleWhere, isWorkspaceAdmin ? undefined : isNotNull(credentialMember.id)) ) const availableCredentials = allAccessibleCredentials diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 0038755e603..7e51de7093c 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -120,6 +120,7 @@ import { assertActiveWorkspaceAccess, getUsersWithPermissions, getWorkspaceWithOwner, + hasWorkspaceAdminAccess, } from '@/lib/workspaces/permissions/utils' import { computeNeedsRedeployment } from '@/app/api/workflows/utils' import { getAllBlocks } from '@/blocks/registry' @@ -1020,7 +1021,7 @@ export class WorkspaceVFS { * Resolve the set of folder IDs that are effectively locked — locked directly * or via a locked ancestor folder. A workflow inside any of these folders is * itself immutable, so its meta.json must report `locked: true`. Mirrors the - * folder-chain walk in `@sim/workflow-authz` getFolderLockStatus, but resolves + * folder-chain walk in `@sim/platform-authz/workflow` getFolderLockStatus, but resolves * the whole workspace in memory to avoid a per-workflow DB round trip. */ private computeLockedFolderIds( @@ -2151,9 +2152,10 @@ export class WorkspaceVFS { envVariables: WorkspaceMdData['envVariables'] }> { try { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(userId, workspaceId) const [envCredentials, oauthCredentials, apiKeyRows, envData] = await Promise.all([ - getAccessibleEnvCredentials(workspaceId, userId), - getAccessibleOAuthCredentials(workspaceId, userId), + getAccessibleEnvCredentials(workspaceId, userId, { isWorkspaceAdmin }), + getAccessibleOAuthCredentials(workspaceId, userId, { isWorkspaceAdmin }), listApiKeys(workspaceId), getPersonalAndWorkspaceEnv(userId, workspaceId), ]) diff --git a/apps/sim/lib/credentials/access.test.ts b/apps/sim/lib/credentials/access.test.ts new file mode 100644 index 00000000000..fcba37053f8 --- /dev/null +++ b/apps/sim/lib/credentials/access.test.ts @@ -0,0 +1,118 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCheckWorkspaceAccess, dbState } = vi.hoisted(() => ({ + mockCheckWorkspaceAccess: vi.fn(), + dbState: { results: [] as any[][] }, +})) + +function makeChain() { + const chain: any = {} + chain.from = vi.fn(() => chain) + chain.where = vi.fn(() => chain) + chain.limit = vi.fn(() => Promise.resolve(dbState.results.shift() ?? [])) + return chain +} + +vi.mock('@sim/db', () => ({ + db: { select: vi.fn(() => makeChain()) }, +})) + +vi.mock('@sim/db/schema', () => ({ + credentialTypeEnum: { + enumValues: ['oauth', 'env_workspace', 'env_personal', 'service_account'], + }, + credential: { + id: 'credential.id', + workspaceId: 'credential.workspaceId', + type: 'credential.type', + }, + credentialMember: { + credentialId: 'credentialMember.credentialId', + userId: 'credentialMember.userId', + status: 'credentialMember.status', + role: 'credentialMember.role', + }, +})) + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...args: unknown[]) => ({ and: args })), + eq: vi.fn((a: unknown, b: unknown) => ({ eq: [a, b] })), + inArray: vi.fn((a: unknown, b: unknown) => ({ inArray: [a, b] })), +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + checkWorkspaceAccess: mockCheckWorkspaceAccess, +})) + +import { getCredentialActorContext } from '@/lib/credentials/access' + +const workspaceAdminAccess = { hasAccess: true, canWrite: true, canAdmin: true } +const noWorkspaceAccess = { hasAccess: false, canWrite: false, canAdmin: false } + +describe('getCredentialActorContext', () => { + beforeEach(() => { + vi.clearAllMocks() + dbState.results = [] + }) + + it('treats an explicit credential admin membership as admin', async () => { + dbState.results = [[{ id: 'c1', workspaceId: 'ws', type: 'oauth' }], [{ role: 'admin' }]] + mockCheckWorkspaceAccess.mockResolvedValue({ hasAccess: true, canWrite: true, canAdmin: false }) + + const ctx = await getCredentialActorContext('c1', 'user1') + + expect(ctx.isAdmin).toBe(true) + }) + + it('derives credential admin from workspace admin for shared credentials', async () => { + dbState.results = [[{ id: 'c1', workspaceId: 'ws', type: 'oauth' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(workspaceAdminAccess) + + const ctx = await getCredentialActorContext('c1', 'admin-user') + + expect(ctx.isAdmin).toBe(true) + }) + + it('does not derive credential admin on personal env credentials', async () => { + dbState.results = [[{ id: 'c1', workspaceId: 'ws', type: 'env_personal' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(workspaceAdminAccess) + + const ctx = await getCredentialActorContext('c1', 'admin-user') + + expect(ctx.isAdmin).toBe(false) + }) + + it('is not admin for a non-admin without membership', async () => { + dbState.results = [[{ id: 'c1', workspaceId: 'ws', type: 'oauth' }], []] + mockCheckWorkspaceAccess.mockResolvedValue({ + hasAccess: true, + canWrite: false, + canAdmin: false, + }) + + const ctx = await getCredentialActorContext('c1', 'reader-user') + + expect(ctx.isAdmin).toBe(false) + }) + + it('returns empty context when the credential does not exist', async () => { + dbState.results = [[]] + + const ctx = await getCredentialActorContext('missing', 'user1') + + expect(ctx.credential).toBeNull() + expect(ctx.isAdmin).toBe(false) + expect(mockCheckWorkspaceAccess).not.toHaveBeenCalled() + }) + + it('exposes workspace access flags from checkWorkspaceAccess', async () => { + dbState.results = [[{ id: 'c1', workspaceId: 'ws', type: 'oauth' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(noWorkspaceAccess) + + const ctx = await getCredentialActorContext('c1', 'outsider') + + expect(ctx.hasWorkspaceAccess).toBe(false) + expect(ctx.canWriteWorkspace).toBe(false) + expect(ctx.isAdmin).toBe(false) + }) +}) diff --git a/apps/sim/lib/credentials/access.ts b/apps/sim/lib/credentials/access.ts index 0593160b739..1fd5728982a 100644 --- a/apps/sim/lib/credentials/access.ts +++ b/apps/sim/lib/credentials/access.ts @@ -1,16 +1,45 @@ import { db } from '@sim/db' -import { credential, credentialMember, workspace } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { and, eq, inArray, ne } from 'drizzle-orm' +import { credential, credentialMember, credentialTypeEnum } from '@sim/db/schema' +import { and, eq, inArray } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('CredentialAccess') +import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' type ActiveCredentialMember = typeof credentialMember.$inferSelect type CredentialRecord = typeof credential.$inferSelect +export type CredentialType = (typeof credentialTypeEnum.enumValues)[number] + +/** + * Credential types shared at the workspace level — every type except a user's + * personal env vars. Derived from the enum so a newly added credential type is + * treated as shared by default, keeping visibility, role, and admin derivation + * consistent instead of drifting against a hand-maintained inclusion list. + */ +export const SHARED_CREDENTIAL_TYPES = credentialTypeEnum.enumValues.filter( + (type) => type !== 'env_personal' +) + +/** Whether a credential is shared at the workspace level (i.e. not a personal env var). */ +export function isSharedCredentialType(type: CredentialType): boolean { + return type !== 'env_personal' +} + +/** + * Whether a user is an admin of a credential: an explicit credential-member admin, + * or — for shared credentials only — a workspace admin (workspace admins are + * derived credential admins, but never for personal env vars). + */ +export function deriveCredentialAdmin(params: { + credentialType: CredentialType + memberRole: ActiveCredentialMember['role'] | null | undefined + workspaceCanAdmin: boolean +}): boolean { + return ( + params.memberRole === 'admin' || + (isSharedCredentialType(params.credentialType) && params.workspaceCanAdmin) + ) +} + export interface CredentialActorContext { credential: CredentialRecord | null member: ActiveCredentialMember | null @@ -20,11 +49,14 @@ export interface CredentialActorContext { } /** - * Resolves user access context for a credential. + * Resolves user access context for a credential. Pass `workspaceAccess` when the + * caller has already resolved access for the credential's workspace to skip a + * redundant lookup; it is reused only when it matches the credential's workspace. */ export async function getCredentialActorContext( credentialId: string, - userId: string + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } ): Promise { const [credentialRow] = await db .select() @@ -42,7 +74,11 @@ export async function getCredentialActorContext( } } - const workspaceAccess = await checkWorkspaceAccess(credentialRow.workspaceId, userId) + const providedAccess = options?.workspaceAccess + const workspaceAccess = + providedAccess && providedAccess.workspace?.id === credentialRow.workspaceId + ? providedAccess + : await checkWorkspaceAccess(credentialRow.workspaceId, userId) const [memberRow] = await db .select() .from(credentialMember) @@ -55,7 +91,11 @@ export async function getCredentialActorContext( ) .limit(1) - const isAdmin = memberRow?.role === 'admin' + const isAdmin = deriveCredentialAdmin({ + credentialType: credentialRow.type, + memberRole: memberRow?.role, + workspaceCanAdmin: workspaceAccess.canAdmin, + }) return { credential: credentialRow, @@ -67,103 +107,29 @@ export async function getCredentialActorContext( } /** - * Revokes all credential memberships for a user across a workspace. - * Before revoking, ensures the workspace owner is an admin on any credential - * where the removed user is the sole active admin, preventing orphaned credentials. + * Revokes all credential memberships for a user across one or more workspaces. + * Workspace owners and admins are derived credential admins, so no per-credential + * owner promotion is needed to avoid orphaning a credential. Returns the number + * of memberships revoked. */ -export async function revokeWorkspaceCredentialMemberships( - workspaceId: string, - userId: string -): Promise { - await revokeWorkspaceCredentialMembershipsTx(db, workspaceId, userId) -} - export async function revokeWorkspaceCredentialMembershipsTx( tx: DbOrTx, - workspaceId: string, + workspaceId: string | string[], userId: string -): Promise { +): Promise { + const workspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId] + if (workspaceIds.length === 0) return 0 + const workspaceCredentialIds = await tx .select({ id: credential.id }) .from(credential) - .where(eq(credential.workspaceId, workspaceId)) + .where(inArray(credential.workspaceId, workspaceIds)) - if (workspaceCredentialIds.length === 0) return + if (workspaceCredentialIds.length === 0) return 0 const credIds = workspaceCredentialIds.map((c) => c.id) - const [workspaceRow] = await tx - .select({ ownerId: workspace.ownerId }) - .from(workspace) - .where(eq(workspace.id, workspaceId)) - .limit(1) - - const ownerId = workspaceRow?.ownerId - - if (ownerId && ownerId !== userId) { - const userAdminMemberships = await tx - .select({ credentialId: credentialMember.credentialId }) - .from(credentialMember) - .where( - and( - eq(credentialMember.userId, userId), - eq(credentialMember.role, 'admin'), - eq(credentialMember.status, 'active'), - inArray(credentialMember.credentialId, credIds) - ) - ) - - for (const { credentialId: credId } of userAdminMemberships) { - const otherAdmins = await tx - .select({ id: credentialMember.id }) - .from(credentialMember) - .where( - and( - eq(credentialMember.credentialId, credId), - eq(credentialMember.role, 'admin'), - eq(credentialMember.status, 'active'), - ne(credentialMember.userId, userId) - ) - ) - .limit(1) - - if (otherAdmins.length > 0) continue - - const now = new Date() - const [existingOwnerMembership] = await tx - .select({ id: credentialMember.id, status: credentialMember.status }) - .from(credentialMember) - .where(and(eq(credentialMember.credentialId, credId), eq(credentialMember.userId, ownerId))) - .limit(1) - - if (existingOwnerMembership) { - await tx - .update(credentialMember) - .set({ role: 'admin', status: 'active', updatedAt: now }) - .where(eq(credentialMember.id, existingOwnerMembership.id)) - } else { - await tx.insert(credentialMember).values({ - id: generateId(), - credentialId: credId, - userId: ownerId, - role: 'admin', - status: 'active', - joinedAt: now, - invitedBy: ownerId, - createdAt: now, - updatedAt: now, - }) - } - - logger.info('Assigned workspace owner as credential admin before member removal', { - credentialId: credId, - ownerId, - removedUserId: userId, - }) - } - } - - await tx + const revoked = await tx .update(credentialMember) .set({ status: 'revoked', updatedAt: new Date() }) .where( @@ -173,4 +139,7 @@ export async function revokeWorkspaceCredentialMembershipsTx( inArray(credentialMember.credentialId, credIds) ) ) + .returning({ id: credentialMember.id }) + + return revoked.length } diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 1910ccd2903..5fd7b711adf 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -1,25 +1,19 @@ import { db } from '@sim/db' import { credential, credentialMember, permissions, workspace } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, eq, inArray, isNull, notInArray, sql } from 'drizzle-orm' +import { and, eq, inArray, isNotNull, isNull, notInArray, or, sql } from 'drizzle-orm' +import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' export interface WorkspaceMembership { ownerId: string | null /** All workspace members: the owner plus everyone with a workspace permission. */ memberUserIds: string[] - /** - * Members who default to a credential **admin** role on shared workspace - * credentials (secrets and service accounts): the owner plus anyone with - * workspace `admin` permission. Manual per-credential overrides are preserved - * separately on re-sync. - */ - adminUserIds: Set } /** - * Resolves a workspace's membership in one owner lookup + one permissions scan, - * returning both the full member set and the admin-defaulting subset (owner + - * workspace `admin` permission). + * Resolves a workspace's membership in one owner lookup + one permissions scan. + * Credential-admin status is derived from workspace role at access time, so + * members are seeded only for use access (the owner plus permission holders). */ export async function getWorkspaceMembership(workspaceId: string): Promise { const [workspaceRows, permissionRows] = await Promise.all([ @@ -29,22 +23,18 @@ export async function getWorkspaceMembership(workspaceId: string): Promise(permissionRows.map((row) => row.userId)) - const adminUserIds = new Set( - permissionRows.filter((row) => row.permissionType === 'admin').map((row) => row.userId) - ) if (ownerId) { memberUserIds.add(ownerId) - adminUserIds.add(ownerId) } - return { ownerId, memberUserIds: Array.from(memberUserIds), adminUserIds } + return { ownerId, memberUserIds: Array.from(memberUserIds) } } export interface WorkspaceEnvKeyAdminAccess { @@ -132,7 +122,6 @@ export async function getUserWorkspaceIds(userId: string): Promise { async function ensureWorkspaceCredentialMemberships( credentialId: string, memberUserIds: string[], - adminUserIds: Set, invitedBy: string ) { if (!memberUserIds.length) return @@ -162,7 +151,7 @@ async function ensureWorkspaceCredentialMemberships( id: generateId(), credentialId, userId: memberUserId, - role: (adminUserIds.has(memberUserId) ? 'admin' : 'member') as 'admin' | 'member', + role: 'member' as const, status: 'active' as const, joinedAt: now, invitedBy, @@ -191,7 +180,7 @@ export async function syncWorkspaceEnvCredentials(params: { actingUserId: string }) { const { workspaceId, envKeys, actingUserId } = params - const { ownerId, memberUserIds, adminUserIds } = await getWorkspaceMembership(workspaceId) + const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId) if (!ownerId) return @@ -242,7 +231,7 @@ export async function syncWorkspaceEnvCredentials(params: { } for (const credentialId of credentialIdsToEnsureMembership) { - await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, adminUserIds, ownerId) + await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, ownerId) } if (normalizedKeys.length > 0) { @@ -276,7 +265,7 @@ export async function createWorkspaceEnvCredentials(params: { const keys = Array.from(new Set(newKeys.filter(Boolean))) if (keys.length === 0) return - const { ownerId, memberUserIds, adminUserIds } = await getWorkspaceMembership(workspaceId) + const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId) if (!ownerId) return @@ -308,9 +297,7 @@ export async function createWorkspaceEnvCredentials(params: { id: generateId(), credentialId, userId: memberUserId, - role: (adminUserIds.has(memberUserId) || memberUserId === actingUserId - ? 'admin' - : 'member') as 'admin' | 'member', + role: (memberUserId === actingUserId ? 'admin' : 'member') as 'admin' | 'member', status: 'active' as const, joinedAt: now, invitedBy: actingUserId, @@ -442,8 +429,12 @@ export async function syncPersonalEnvCredentialsForUser(params: { export async function getAccessibleEnvCredentials( workspaceId: string, - userId: string + userId: string, + options?: { isWorkspaceAdmin?: boolean } ): Promise { + const isWorkspaceAdmin = + options?.isWorkspaceAdmin ?? (await hasWorkspaceAdminAccess(userId, workspaceId)) + const rows = await db .select({ type: credential.type, @@ -452,7 +443,7 @@ export async function getAccessibleEnvCredentials( updatedAt: credential.updatedAt, }) .from(credential) - .innerJoin( + .leftJoin( credentialMember, and( eq(credentialMember.credentialId, credential.id), @@ -463,18 +454,23 @@ export async function getAccessibleEnvCredentials( .where( and( eq(credential.workspaceId, workspaceId), - inArray(credential.type, ['env_workspace', 'env_personal']) + inArray(credential.type, ['env_workspace', 'env_personal']), + or( + isNotNull(credentialMember.id), + eq(credential.envOwnerUserId, userId), + isWorkspaceAdmin ? eq(credential.type, 'env_workspace') : undefined + ) ) ) return rows .filter( - (row): row is AccessibleEnvCredential => - (row.type === 'env_workspace' || row.type === 'env_personal') && Boolean(row.envKey) + (row): row is typeof row & { type: 'env_workspace' | 'env_personal'; envKey: string } => + row.envKey !== null && (row.type === 'env_workspace' || row.type === 'env_personal') ) .map((row) => ({ type: row.type, - envKey: row.envKey!, + envKey: row.envKey, envOwnerUserId: row.envOwnerUserId, updatedAt: row.updatedAt, })) @@ -490,8 +486,39 @@ export interface AccessibleOAuthCredential { export async function getAccessibleOAuthCredentials( workspaceId: string, - userId: string + userId: string, + options?: { isWorkspaceAdmin?: boolean } ): Promise { + const isWorkspaceAdmin = + options?.isWorkspaceAdmin ?? (await hasWorkspaceAdminAccess(userId, workspaceId)) + + if (isWorkspaceAdmin) { + const rows = await db + .select({ + id: credential.id, + providerId: credential.providerId, + displayName: credential.displayName, + updatedAt: credential.updatedAt, + }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + inArray(credential.type, ['oauth', 'service_account']) + ) + ) + + return rows + .filter((row): row is typeof row & { providerId: string } => Boolean(row.providerId)) + .map((row) => ({ + id: row.id, + providerId: row.providerId, + displayName: row.displayName, + role: 'admin' as const, + updatedAt: row.updatedAt, + })) + } + const rows = await db .select({ id: credential.id, diff --git a/apps/sim/lib/environment/utils.ts b/apps/sim/lib/environment/utils.ts index 20a5e4e95ae..f85989cecb0 100644 --- a/apps/sim/lib/environment/utils.ts +++ b/apps/sim/lib/environment/utils.ts @@ -11,7 +11,7 @@ import { getAccessibleEnvCredentials, syncPersonalEnvCredentialsForUser, } from '@/lib/credentials/environment' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('EnvironmentUtils') const EFFECTIVE_DECRYPTED_ENV_CACHE_TTL_MS = 2_000 @@ -92,7 +92,8 @@ export async function getEnvironmentVariableKeys(userId: string): Promise<{ export async function getPersonalAndWorkspaceEnv( userId: string, - workspaceId?: string + workspaceId?: string, + options?: { workspaceAccess?: WorkspaceAccess } ): Promise<{ personalEncrypted: Record workspaceEncrypted: Record @@ -101,11 +102,13 @@ export async function getPersonalAndWorkspaceEnv( conflicts: string[] decryptionFailures: string[] }> { + let workspaceCanAdmin = false if (workspaceId) { - const access = await checkWorkspaceAccess(workspaceId, userId) + const access = options?.workspaceAccess ?? (await checkWorkspaceAccess(workspaceId, userId)) if (!access.hasAccess) { throw new Error(`Access denied to workspace ${workspaceId}`) } + workspaceCanAdmin = access.canAdmin } const [personalRows, workspaceRows, accessibleEnvCredentials] = await Promise.all([ @@ -117,7 +120,9 @@ export async function getPersonalAndWorkspaceEnv( .where(eq(workspaceEnvironment.workspaceId, workspaceId)) .limit(1) : Promise.resolve([] as any[]), - workspaceId ? getAccessibleEnvCredentials(workspaceId, userId) : Promise.resolve([]), + workspaceId + ? getAccessibleEnvCredentials(workspaceId, userId, { isWorkspaceAdmin: workspaceCanAdmin }) + : Promise.resolve([]), ]) const ownPersonalEncrypted: Record = (personalRows[0]?.variables as any) || {} @@ -165,7 +170,7 @@ export async function getPersonalAndWorkspaceEnv( let workspaceEncrypted: Record = allWorkspaceEncrypted if (hasCredentialFiltering) { - personalEncrypted = {} + personalEncrypted = { ...ownPersonalEncrypted } for (const [envKey, ownerUserId] of selectedPersonalOwners.entries()) { const ownerVariables = ownerVariablesByUserId.get(ownerUserId) const encryptedValue = ownerVariables?.[envKey] diff --git a/apps/sim/lib/execution/preprocessing.test.ts b/apps/sim/lib/execution/preprocessing.test.ts index e72c44a567e..223f8e7e6c6 100644 --- a/apps/sim/lib/execution/preprocessing.test.ts +++ b/apps/sim/lib/execution/preprocessing.test.ts @@ -39,7 +39,7 @@ vi.mock('@/lib/workspaces/utils', () => ({ getWorkspaceBilledAccountUserId: mockGetWorkspaceBilledAccountUserId, })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ getActiveWorkflowRecord: vi.fn().mockResolvedValue({ id: 'workflow-1', userId: 'creator-1', diff --git a/apps/sim/lib/execution/preprocessing.ts b/apps/sim/lib/execution/preprocessing.ts index a41d3dbbbca..17af2700f2c 100644 --- a/apps/sim/lib/execution/preprocessing.ts +++ b/apps/sim/lib/execution/preprocessing.ts @@ -1,6 +1,6 @@ import type { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { getActiveWorkflowRecord } from '@sim/workflow-authz' +import { getActiveWorkflowRecord } from '@sim/platform-authz/workflow' import { getActivelyBannedUserIds } from '@/lib/auth/ban' import { checkOrgMemberUsageLimit, diff --git a/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts b/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts index 2fea91bfe0a..9b8059d2845 100644 --- a/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts +++ b/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts @@ -28,7 +28,7 @@ vi.mock('@/lib/workspaces/utils', () => ({ getWorkspaceBilledAccountUserId: mockGetWorkspaceBilledAccountUserId, })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ getActiveWorkflowRecord: vi.fn().mockResolvedValue({ id: 'workflow-1', workspaceId: 'workspace-1', diff --git a/apps/sim/lib/invitations/core.ts b/apps/sim/lib/invitations/core.ts index c8152bad844..ae99f6f3b11 100644 --- a/apps/sim/lib/invitations/core.ts +++ b/apps/sim/lib/invitations/core.ts @@ -14,6 +14,7 @@ import { workspaceEnvironment, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { PERMISSION_RANK, type PermissionType } from '@sim/platform-authz/workspace' import { generateId } from '@sim/utils/id' import { normalizeEmail } from '@sim/utils/string' import { and, eq, inArray, lte } from 'drizzle-orm' @@ -33,9 +34,6 @@ import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' const logger = createLogger('InvitationCore') -export const PERMISSION_RANK = { read: 0, write: 1, admin: 2 } as const -export type PermissionLevel = keyof typeof PERMISSION_RANK - export const INVITATION_EXPIRY_DAYS = 7 export function computeInvitationExpiry(daysFromNow = INVITATION_EXPIRY_DAYS): Date { @@ -474,12 +472,12 @@ export async function acceptInvitation( ) .limit(1) - const newPermission = grant.permission as PermissionLevel + const newPermission = grant.permission as PermissionType const newRank = PERMISSION_RANK[newPermission] ?? 0 if (existingPermission) { const existingRank = - PERMISSION_RANK[existingPermission.permissionType as PermissionLevel] ?? 0 + PERMISSION_RANK[existingPermission.permissionType as PermissionType] ?? 0 if (newRank > existingRank) { await tx .update(permissions) diff --git a/apps/sim/lib/invitations/workspace-invitations.ts b/apps/sim/lib/invitations/workspace-invitations.ts index 63de5039649..c60fa866cbd 100644 --- a/apps/sim/lib/invitations/workspace-invitations.ts +++ b/apps/sim/lib/invitations/workspace-invitations.ts @@ -20,6 +20,7 @@ import { import { captureServerEvent } from '@/lib/posthog/server' import { getWorkspaceWithOwner, + hasWorkspaceAdminAccess, type PermissionType, type WorkspaceWithOwner, } from '@/lib/workspaces/permissions/utils' @@ -85,20 +86,8 @@ export async function prepareWorkspaceInvitationContext({ }): Promise { await validateInvitationsAllowed(inviterId, workspaceId) - const userPermission = await db - .select() - .from(permissions) - .where( - and( - eq(permissions.entityId, workspaceId), - eq(permissions.entityType, 'workspace'), - eq(permissions.userId, inviterId), - eq(permissions.permissionType, 'admin') - ) - ) - .then((rows) => rows[0]) - - if (!userPermission) { + const isAdmin = await hasWorkspaceAdminAccess(inviterId, workspaceId) + if (!isAdmin) { throw new WorkspaceInvitationError({ message: 'You need admin permissions to invite users', status: 403, diff --git a/apps/sim/lib/logs/fetch-log-detail.ts b/apps/sim/lib/logs/fetch-log-detail.ts index 36c4efb9fbb..dd90e80180f 100644 --- a/apps/sim/lib/logs/fetch-log-detail.ts +++ b/apps/sim/lib/logs/fetch-log-detail.ts @@ -2,7 +2,6 @@ import { db } from '@sim/db' import { jobExecutionLogs, pausedExecutions, - permissions, usageLog, workflow, workflowDeploymentVersion, @@ -11,6 +10,7 @@ import { import { and, eq, type SQL } from 'drizzle-orm' import type { CostLedger } from '@/lib/api/contracts/logs' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' type LookupColumn = 'id' | 'executionId' @@ -84,6 +84,9 @@ export async function fetchLogDetail({ lookupColumn, lookupValue, }: FetchLogDetailArgs) { + const access = await checkWorkspaceAccess(workspaceId, userId) + if (!access.hasAccess) return null + const workflowMatch: SQL = lookupColumn === 'id' ? eq(workflowExecutionLogs.id, lookupValue) @@ -125,14 +128,6 @@ export async function fetchLogDetail({ eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId) ) .leftJoin(pausedExecutions, eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(and(workflowMatch, eq(workflowExecutionLogs.workspaceId, workspaceId))) .limit(1) @@ -221,14 +216,6 @@ export async function fetchLogDetail({ createdAt: jobExecutionLogs.createdAt, }) .from(jobExecutionLogs) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, jobExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(and(jobMatch, eq(jobExecutionLogs.workspaceId, workspaceId))) .limit(1) diff --git a/apps/sim/lib/logs/list-logs.test.ts b/apps/sim/lib/logs/list-logs.test.ts index b80bde60a9b..d8ad2cdb6cf 100644 --- a/apps/sim/lib/logs/list-logs.test.ts +++ b/apps/sim/lib/logs/list-logs.test.ts @@ -50,6 +50,17 @@ vi.mock('@/lib/logs/folder-expansion', () => ({ expandFolderIdsWithDescendants: vi.fn(async (_ws: string, ids: string | undefined) => ids), })) +// listLogs gates workspace access at entry; the resolver is tested separately. +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + checkWorkspaceAccess: vi.fn(async () => ({ + exists: true, + hasAccess: true, + canWrite: true, + canAdmin: true, + workspace: { id: 'ws-1', name: 'Test', ownerId: 'user-1', organizationId: null }, + })), +})) + import type { ListLogsParams } from './list-logs' import { decodeCursor, listLogs } from './list-logs' diff --git a/apps/sim/lib/logs/list-logs.ts b/apps/sim/lib/logs/list-logs.ts index 0165d60ad73..4989d795f49 100644 --- a/apps/sim/lib/logs/list-logs.ts +++ b/apps/sim/lib/logs/list-logs.ts @@ -2,7 +2,6 @@ import { dbReplica } from '@sim/db' import { jobExecutionLogs, pausedExecutions, - permissions, workflow, workflowDeploymentVersion, workflowExecutionLogs, @@ -33,6 +32,7 @@ import type { import { jobCostTotal } from '@/lib/logs/fetch-log-detail' import { buildFilterConditions } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' export type ListLogsParams = z.output @@ -66,6 +66,11 @@ export function decodeCursor(cursor: string): CursorData | null { * workspace permission via the `permissions` join. */ export async function listLogs(params: ListLogsParams, userId: string): Promise { + const access = await checkWorkspaceAccess(params.workspaceId, userId) + if (!access.hasAccess) { + return { data: [], nextCursor: null } + } + const sortBy = params.sortBy as SortBy const sortOrder = params.sortOrder as SortOrder const cursor = params.cursor ? decodeCursor(params.cursor) : null @@ -221,14 +226,6 @@ export async function listLogs(params: ListLogsParams, userId: string): Promise< eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId) ) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(and(...workflowConditions)) .orderBy(orderByClause(workflowSortExpr), dir(workflowExecutionLogs.id)) .limit(fetchSize) @@ -236,10 +233,6 @@ export async function listLogs(params: ListLogsParams, userId: string): Promise< const jobConditions: SQL[] = [eq(jobExecutionLogs.workspaceId, p.workspaceId)] if (includeJobLogs) { - jobConditions.push( - sql`EXISTS (SELECT 1 FROM ${permissions} WHERE ${permissions.entityType} = 'workspace' AND ${permissions.entityId} = ${jobExecutionLogs.workspaceId} AND ${permissions.userId} = ${userId})` - ) - if (p.level && p.level !== 'all') { const levels = p.level.split(',').filter(Boolean) const jobLevelConditions: SQL[] = [] diff --git a/apps/sim/lib/mcp/middleware.ts b/apps/sim/lib/mcp/middleware.ts index 90367b3cd75..ee75a6b6304 100644 --- a/apps/sim/lib/mcp/middleware.ts +++ b/apps/sim/lib/mcp/middleware.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { type PermissionType, permissionSatisfies } from '@sim/platform-authz/workspace' import { toError } from '@sim/utils/errors' import type { NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' @@ -213,16 +214,7 @@ async function validateMcpAuth( * Check if user has required permission level */ function checkPermissionLevel(userPermission: string, requiredLevel: McpPermissionLevel): boolean { - switch (requiredLevel) { - case 'read': - return ['read', 'write', 'admin'].includes(userPermission) - case 'write': - return ['write', 'admin'].includes(userPermission) - case 'admin': - return userPermission === 'admin' - default: - return false - } + return permissionSatisfies(userPermission as PermissionType, requiredLevel) } /** diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index e82025d853f..86ac2312b2d 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -1,4 +1,4 @@ -import { copilotChats, db, mothershipInboxTask, permissions, user, workspace } from '@sim/db' +import { copilotChats, db, mothershipInboxTask, user, workspace } from '@sim/db' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' @@ -330,20 +330,21 @@ async function resolveUserId( senderEmail: string, ws: { id: string; ownerId: string } ): Promise { - const [member] = await db - .select({ userId: permissions.userId }) - .from(permissions) - .innerJoin(user, eq(permissions.userId, user.id)) - .where( - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, ws.id), - sql`lower(${user.email}) = ${senderEmail.toLowerCase()}` - ) - ) + const [matchedUser] = await db + .select({ id: user.id }) + .from(user) + .where(sql`lower(${user.email}) = ${senderEmail.toLowerCase()}`) + .orderBy(user.createdAt) .limit(1) - return member?.userId ?? ws.ownerId + if (matchedUser) { + const permission = await getUserEntityPermissions(matchedUser.id, 'workspace', ws.id) + if (permission !== null) { + return matchedUser.id + } + } + + return ws.ownerId } /** diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts index d930858fb6a..26947600a0f 100644 --- a/apps/sim/lib/workflows/orchestration/deploy.ts +++ b/apps/sim/lib/workflows/orchestration/deploy.ts @@ -1,7 +1,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db, workflowDeploymentVersion, workflow as workflowTable } from '@sim/db' import { createLogger } from '@sim/logger' -import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz' +import { assertWorkflowMutable, WorkflowLockedError } from '@sim/platform-authz/workflow' import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { env } from '@/lib/core/config/env' diff --git a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts index 5f5524f2846..d3c38cdbbb0 100644 --- a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts +++ b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts @@ -2,9 +2,9 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isFolderInWorkspace } from '@sim/platform-authz/workflow' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { isFolderInWorkspace } from '@sim/workflow-authz' import { and, eq, isNull, min, ne } from 'drizzle-orm' import { generateRequestId } from '@/lib/core/utils/request' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts index 0d409629f77..75a3ad2d340 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.ts @@ -7,8 +7,11 @@ import { workflowSubflows, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { + authorizeWorkflowByWorkspacePermission, + FolderLockedError, +} from '@sim/platform-authz/workflow' import { generateId } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission, FolderLockedError } from '@sim/workflow-authz' import { and, eq, isNull, min } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index 725e7a13479..895a87f6013 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -1,9 +1,9 @@ import { db, runOutsideTransactionContext, workflow, workflowDeploymentVersion } from '@sim/db' import { credential } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getActiveWorkflowContext } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { getActiveWorkflowContext } from '@sim/workflow-authz' import { loadWorkflowFromNormalizedTablesRaw, persistMigratedBlocks, diff --git a/apps/sim/lib/workflows/queries.ts b/apps/sim/lib/workflows/queries.ts index 751cbd23695..dd3bfd141d9 100644 --- a/apps/sim/lib/workflows/queries.ts +++ b/apps/sim/lib/workflows/queries.ts @@ -1,7 +1,8 @@ import { db } from '@sim/db' -import { permissions, workflow } from '@sim/db/schema' +import { workflow } from '@sim/db/schema' import { and, asc, eq, inArray, isNull, sql } from 'drizzle-orm' import type { WorkflowListItem } from '@/lib/api/contracts/workflows' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' type WorkflowListScope = 'active' | 'archived' | 'all' @@ -85,11 +86,8 @@ export async function listWorkflowsForUser({ return rows.map(toListItem) } - const workspacePermissionRows = await db - .select({ workspaceId: permissions.entityId }) - .from(permissions) - .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) - const workspaceIds = workspacePermissionRows.map((row) => row.workspaceId) + const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId, 'all') + const workspaceIds = accessibleRows.map((row) => row.workspace.id) if (workspaceIds.length === 0) return [] const rows = await db diff --git a/apps/sim/lib/workflows/utils.test.ts b/apps/sim/lib/workflows/utils.test.ts index 1af508d898b..0936d440680 100644 --- a/apps/sim/lib/workflows/utils.test.ts +++ b/apps/sim/lib/workflows/utils.test.ts @@ -20,7 +20,7 @@ const { mockAuthorizeWorkflow } = vi.hoisted(() => ({ mockAuthorizeWorkflow: vi.fn(), })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflow, getActiveWorkflowContext: vi.fn(), getActiveWorkflowRecord: vi.fn(), diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts index 2f20e012a99..ced3f838e93 100644 --- a/apps/sim/lib/workflows/utils.ts +++ b/apps/sim/lib/workflows/utils.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' -import { permissions, workflowFolder, workflow as workflowTable } from '@sim/db/schema' +import { workflowFolder, workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { generateId } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, asc, eq, inArray, isNull, max, min, sql } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -11,6 +11,7 @@ import { materializeInlineExecutionValue } from '@/lib/execution/payloads/inline import type { ExecutionMaterializationContext } from '@/lib/execution/payloads/materialization.server' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' +import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' import type { ExecutionResult } from '@/executor/types' const logger = createLogger('WorkflowUtils') @@ -161,12 +162,8 @@ export async function resolveWorkflowIdForUser( } } - const workspaceIds = await db - .select({ entityId: permissions.entityId }) - .from(permissions) - .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) - - const workspaceIdList = workspaceIds.map((row) => row.entityId) + const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId, 'all') + const workspaceIdList = accessibleRows.map((row) => row.workspace.id) const allowedWorkspaceIds = workspaceId ? workspaceIdList.filter((candidateWorkspaceId) => candidateWorkspaceId === workspaceId) : workspaceIdList diff --git a/apps/sim/lib/workspace-events/emitter.test.ts b/apps/sim/lib/workspace-events/emitter.test.ts index 9fc893ee6a2..096c79fb4d8 100644 --- a/apps/sim/lib/workspace-events/emitter.test.ts +++ b/apps/sim/lib/workspace-events/emitter.test.ts @@ -19,7 +19,7 @@ const { mockProcessPolledWebhookEvent: vi.fn(), })) -vi.mock('@sim/workflow-authz', () => ({ +vi.mock('@sim/platform-authz/workflow', () => ({ getActiveWorkflowContext: mockGetActiveWorkflowContext, })) diff --git a/apps/sim/lib/workspace-events/emitter.ts b/apps/sim/lib/workspace-events/emitter.ts index 4c9aa07e440..b2e0f5d1bbc 100644 --- a/apps/sim/lib/workspace-events/emitter.ts +++ b/apps/sim/lib/workspace-events/emitter.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' +import { getActiveWorkflowContext } from '@sim/platform-authz/workflow' import { generateShortId } from '@sim/utils/id' -import { getActiveWorkflowContext } from '@sim/workflow-authz' import type { WorkflowExecutionLog } from '@/lib/logs/types' import { isSimRuleEventType, diff --git a/apps/sim/lib/workspaces/organization/utils.ts b/apps/sim/lib/workspaces/organization/utils.ts index 21472fbf0d8..27ccb51396b 100644 --- a/apps/sim/lib/workspaces/organization/utils.ts +++ b/apps/sim/lib/workspaces/organization/utils.ts @@ -3,6 +3,7 @@ * These are pure functions that compute values from organization data */ +import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { quickValidateEmail } from '@/lib/messaging/email/validation' import type { Organization } from '@/lib/workspaces/organization/types' @@ -28,7 +29,7 @@ export function isAdminOrOwner( userEmail?: string ): boolean { const role = getUserRole(organization, userEmail) - return role === 'owner' || role === 'admin' + return isOrgAdminRole(role) } /** diff --git a/apps/sim/lib/workspaces/permissions/utils.test.ts b/apps/sim/lib/workspaces/permissions/utils.test.ts index 1a9c8c96f9f..503be8036aa 100644 --- a/apps/sim/lib/workspaces/permissions/utils.test.ts +++ b/apps/sim/lib/workspaces/permissions/utils.test.ts @@ -7,7 +7,6 @@ import { getUsersWithPermissions, getWorkspaceById, getWorkspaceWithOwner, - hasAdminPermission, hasWorkspaceAdminAccess, workspaceExists, } from '@/lib/workspaces/permissions/utils' @@ -54,7 +53,7 @@ describe('Permission Utils', () => { const chain = createMockChain(mockResults) mockDb.select.mockReturnValue(chain) - const result = await getUserEntityPermissions('user123', 'workspace', 'workspace456') + const result = await getUserEntityPermissions('user123', 'workflow', 'workflow456') expect(result).toBe('admin') }) @@ -78,7 +77,7 @@ describe('Permission Utils', () => { const chain = createMockChain(mockResults) mockDb.select.mockReturnValue(chain) - const result = await getUserEntityPermissions('user999', 'workspace', 'workspace999') + const result = await getUserEntityPermissions('user999', 'workflow', 'workflow999') expect(result).toBe('admin') }) @@ -101,7 +100,7 @@ describe('Permission Utils', () => { const chain = createMockChain(mockResults) mockDb.select.mockReturnValue(chain) - const result = await getUserEntityPermissions('user123', 'workspace', 'workspace456') + const result = await getUserEntityPermissions('user123', 'workflow', 'workflow456') expect(result).toBe('write') }) @@ -137,86 +136,37 @@ describe('Permission Utils', () => { }) }) - describe('hasAdminPermission', () => { - it('should return true when user has admin permission for workspace', async () => { - const chain = createMockChain([{ id: 'perm1' }]) - mockDb.select.mockReturnValue(chain) - - const result = await hasAdminPermission('admin-user', 'workspace123') - - expect(result).toBe(true) - }) - - it('should return false when user has no admin permission for workspace', async () => { - const chain = createMockChain([]) - mockDb.select.mockReturnValue(chain) - - const result = await hasAdminPermission('regular-user', 'workspace123') - - expect(result).toBe(false) - }) - - it('should return false when user has write permission but not admin', async () => { - const chain = createMockChain([]) - mockDb.select.mockReturnValue(chain) - - const result = await hasAdminPermission('write-user', 'workspace123') - - expect(result).toBe(false) - }) - - it('should return false when user has read permission but not admin', async () => { - const chain = createMockChain([]) - mockDb.select.mockReturnValue(chain) - - const result = await hasAdminPermission('read-user', 'workspace123') - - expect(result).toBe(false) - }) - - it('should handle non-existent workspace', async () => { - const chain = createMockChain([]) - mockDb.select.mockReturnValue(chain) - - const result = await hasAdminPermission('user123', 'non-existent-workspace') - - expect(result).toBe(false) - }) - - it('should handle empty user ID', async () => { - const chain = createMockChain([]) - mockDb.select.mockReturnValue(chain) - - const result = await hasAdminPermission('', 'workspace123') + describe('getUsersWithPermissions', () => { + function mockSelectSequence(results: any[][]) { + let index = 0 + mockDb.select.mockImplementation(() => createMockChain(results[index++] ?? [])) + } - expect(result).toBe(false) - }) - }) + const joinedAt = new Date('2026-04-22T00:00:00.000Z') - describe('getUsersWithPermissions', () => { - it('should return empty array when no users have permissions for workspace', async () => { - const usersChain = createMockChain([]) - mockDb.select.mockReturnValue(usersChain) + it('should return empty array when the workspace does not exist', async () => { + mockSelectSequence([[]]) const result = await getUsersWithPermissions('workspace123') expect(result).toEqual([]) }) - it('should return users with their permissions for workspace', async () => { - const mockUsersResults = [ - { - userId: 'user1', - email: 'alice@example.com', - name: 'Alice Smith', - image: 'https://example.com/alice.png', - permissionType: 'admin' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - }, - ] - - const usersChain = createMockChain(mockUsersResults) - mockDb.select.mockReturnValue(usersChain) + it('should return users with their explicit permissions for a personal workspace', async () => { + mockSelectSequence([ + [{ id: 'workspace456', ownerId: 'owner-user', organizationId: null }], + [ + { + userId: 'user1', + email: 'alice@example.com', + name: 'Alice Smith', + image: 'https://example.com/alice.png', + permissionType: 'admin' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + ], + ]) const result = await getUsersWithPermissions('workspace456') @@ -229,111 +179,165 @@ describe('Permission Utils', () => { permissionType: 'admin', isExternal: false, joinedAt: '2026-04-22T00:00:00.000Z', + roleSource: 'explicit', }, ]) }) - it('marks users as external when they are not members of the workspace organization', async () => { - const mockUsersResults = [ - { - userId: 'internal-user', - email: 'internal@example.com', - name: 'Internal User', - image: null, - permissionType: 'admin' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - workspaceOrganizationId: 'org-1', - workspaceOwnerId: 'internal-user', - userOrganizationId: 'org-1', - }, - { - userId: 'external-user', - email: 'external@example.com', - name: 'External User', - image: null, - permissionType: 'write' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - workspaceOrganizationId: 'org-1', - workspaceOwnerId: 'internal-user', - userOrganizationId: 'org-2', - }, - ] - - const usersChain = createMockChain(mockUsersResults) - mockDb.select.mockReturnValue(usersChain) + it('tags the workspace owner with roleSource owner', async () => { + mockSelectSequence([ + [{ id: 'workspace456', ownerId: 'user1', organizationId: null }], + [ + { + userId: 'user1', + email: 'owner@example.com', + name: 'Owner', + image: null, + permissionType: 'admin' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + ], + ]) const result = await getUsersWithPermissions('workspace456') - expect(result.map((u) => ({ email: u.email, isExternal: u.isExternal }))).toEqual([ - { email: 'internal@example.com', isExternal: false }, - { email: 'external@example.com', isExternal: true }, + expect(result[0].roleSource).toBe('owner') + }) + + it('merges organization admins as derived workspace admins', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'owner-user', organizationId: 'org-1' }], + [ + { + userId: 'member-user', + email: 'member@example.com', + name: 'Member', + image: null, + permissionType: 'read' as PermissionType, + joinedAt, + userOrganizationId: 'org-1', + }, + ], + [ + { + userId: 'org-admin-user', + email: 'orgadmin@example.com', + name: 'Org Admin', + image: null, + joinedAt, + }, + ], ]) - }) - - it('marks a non-owner member of another org as external on a personal workspace', async () => { - const mockUsersResults = [ - { - userId: 'owner-user', - email: 'owner@example.com', - name: 'Owner', - image: null, - permissionType: 'admin' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - workspaceOrganizationId: null, - workspaceOwnerId: 'owner-user', - userOrganizationId: null, - }, - { - userId: 'guest-user', - email: 'guest@example.com', - name: 'Guest', - image: null, - permissionType: 'write' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - workspaceOrganizationId: null, - workspaceOwnerId: 'owner-user', - userOrganizationId: 'org-guest', - }, - ] - const usersChain = createMockChain(mockUsersResults) - mockDb.select.mockReturnValue(usersChain) + const result = await getUsersWithPermissions('ws') + const orgAdmin = result.find((u) => u.userId === 'org-admin-user') - const result = await getUsersWithPermissions('workspace-personal') + expect(orgAdmin).toMatchObject({ + permissionType: 'admin', + roleSource: 'org-admin', + isExternal: false, + }) + }) - expect(result.map((u) => ({ email: u.email, isExternal: u.isExternal }))).toEqual([ - { email: 'owner@example.com', isExternal: false }, - { email: 'guest@example.com', isExternal: true }, + it('marks users as external when they are not members of the workspace organization', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'internal-user', organizationId: 'org-1' }], + [ + { + userId: 'internal-user', + email: 'internal@example.com', + name: 'Internal User', + image: null, + permissionType: 'admin' as PermissionType, + joinedAt, + userOrganizationId: 'org-1', + }, + { + userId: 'external-user', + email: 'external@example.com', + name: 'External User', + image: null, + permissionType: 'write' as PermissionType, + joinedAt, + userOrganizationId: 'org-2', + }, + ], + [], ]) + + const result = await getUsersWithPermissions('ws') + const byEmail = new Map(result.map((u) => [u.email, u.isExternal])) + + expect(byEmail.get('internal@example.com')).toBe(false) + expect(byEmail.get('external@example.com')).toBe(true) }) - it('should return multiple users with different permission levels', async () => { - const mockUsersResults = [ - { - userId: 'user1', - email: 'admin@example.com', - name: 'Admin User', - permissionType: 'admin' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - }, - { - userId: 'user2', - email: 'writer@example.com', - name: 'Writer User', - permissionType: 'write' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - }, - { - userId: 'user3', - email: 'reader@example.com', - name: 'Reader User', - permissionType: 'read' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - }, - ] + it('marks a non-owner member of another org as external on a personal workspace', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'owner-user', organizationId: null }], + [ + { + userId: 'owner-user', + email: 'owner@example.com', + name: 'Owner', + image: null, + permissionType: 'admin' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + { + userId: 'guest-user', + email: 'guest@example.com', + name: 'Guest', + image: null, + permissionType: 'write' as PermissionType, + joinedAt, + userOrganizationId: 'org-guest', + }, + ], + ]) - const usersChain = createMockChain(mockUsersResults) - mockDb.select.mockReturnValue(usersChain) + const result = await getUsersWithPermissions('workspace-personal') + const byEmail = new Map(result.map((u) => [u.email, u.isExternal])) + + expect(byEmail.get('owner@example.com')).toBe(false) + expect(byEmail.get('guest@example.com')).toBe(true) + }) + + it('should return multiple users sorted by email', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'owner-user', organizationId: null }], + [ + { + userId: 'user1', + email: 'a-admin@example.com', + name: 'Admin User', + image: null, + permissionType: 'admin' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + { + userId: 'user2', + email: 'b-writer@example.com', + name: 'Writer User', + image: null, + permissionType: 'write' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + { + userId: 'user3', + email: 'c-reader@example.com', + name: 'Reader User', + image: null, + permissionType: 'read' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + ], + ]) const result = await getUsersWithPermissions('workspace456') @@ -344,18 +348,20 @@ describe('Permission Utils', () => { }) it('should handle users with empty names', async () => { - const mockUsersResults = [ - { - userId: 'user1', - email: 'test@example.com', - name: '', - permissionType: 'read' as PermissionType, - joinedAt: new Date('2026-04-22T00:00:00.000Z'), - }, - ] - - const usersChain = createMockChain(mockUsersResults) - mockDb.select.mockReturnValue(usersChain) + mockSelectSequence([ + [{ id: 'ws', ownerId: 'owner-user', organizationId: null }], + [ + { + userId: 'user1', + email: 'test@example.com', + name: '', + image: null, + permissionType: 'read' as PermissionType, + joinedAt, + userOrganizationId: null, + }, + ], + ]) const result = await getUsersWithPermissions('workspace123') @@ -364,9 +370,15 @@ describe('Permission Utils', () => { }) describe('hasWorkspaceAdminAccess', () => { - it('should return true when user owns the workspace', async () => { - const chain = createMockChain([{ ownerId: 'user123' }]) - mockDb.select.mockReturnValue(chain) + it('should return true for the workspace owner via their explicit admin row', async () => { + let callCount = 0 + mockDb.select.mockImplementation(() => { + callCount++ + if (callCount === 1) { + return createMockChain([{ ownerId: 'user123' }]) + } + return createMockChain([{ permissionType: 'admin' }]) + }) const result = await hasWorkspaceAdminAccess('user123', 'workspace456') @@ -380,7 +392,7 @@ describe('Permission Utils', () => { if (callCount === 1) { return createMockChain([{ ownerId: 'other-user' }]) } - return createMockChain([{ id: 'perm1' }]) + return createMockChain([{ permissionType: 'admin' }]) }) const result = await hasWorkspaceAdminAccess('user123', 'workspace456') @@ -647,7 +659,7 @@ describe('Permission Utils', () => { const result = await getManageableWorkspaces('user123') - expect(result).toHaveLength(2) // Should include duplicates from admin permissions + expect(result).toHaveLength(1) }) it('should handle empty user ID gracefully', async () => { @@ -758,13 +770,20 @@ describe('Permission Utils', () => { exists: false, hasAccess: false, canWrite: false, + canAdmin: false, workspace: null, }) }) - it('should return full access when user is workspace owner', async () => { - const chain = createMockChain([{ id: 'workspace123', ownerId: 'user123' }]) - mockDb.select.mockReturnValue(chain) + it('should return full access for the workspace owner via their explicit admin row', async () => { + let callCount = 0 + mockDb.select.mockImplementation(() => { + callCount++ + if (callCount === 1) { + return createMockChain([{ id: 'workspace123', ownerId: 'user123' }]) + } + return createMockChain([{ permissionType: 'admin' }]) + }) const result = await checkWorkspaceAccess('workspace123', 'user123') @@ -772,6 +791,7 @@ describe('Permission Utils', () => { exists: true, hasAccess: true, canWrite: true, + canAdmin: true, workspace: { id: 'workspace123', ownerId: 'user123' }, }) }) @@ -864,4 +884,70 @@ describe('Permission Utils', () => { expect(result.hasAccess).toBe(false) }) }) + + describe('organization admin inheritance', () => { + function mockSelectSequence(results: any[][]) { + let index = 0 + mockDb.select.mockImplementation(() => createMockChain(results[index++] ?? [])) + } + + it('checkWorkspaceAccess grants admin to org admins without an explicit row', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'other-user', organizationId: 'org-1' }], + [], + [{ role: 'admin' }], + ]) + + const result = await checkWorkspaceAccess('ws', 'org-admin-user') + + expect(result.hasAccess).toBe(true) + expect(result.canWrite).toBe(true) + expect(result.canAdmin).toBe(true) + }) + + it('getUserEntityPermissions returns admin for an org owner without an explicit row', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'other-user', organizationId: 'org-1' }], + [], + [{ role: 'owner' }], + ]) + + const result = await getUserEntityPermissions('org-owner-user', 'workspace', 'ws') + + expect(result).toBe('admin') + }) + + it('hasWorkspaceAdminAccess is true for an org admin of the workspace org', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'other-user', organizationId: 'org-1' }], + [], + [{ role: 'admin' }], + ]) + + const result = await hasWorkspaceAdminAccess('org-admin-user', 'ws') + + expect(result).toBe(true) + }) + + it('does not elevate a plain org member', async () => { + mockSelectSequence([ + [{ id: 'ws', ownerId: 'other-user', organizationId: 'org-1' }], + [], + [{ role: 'member' }], + ]) + + const result = await checkWorkspaceAccess('ws', 'org-member-user') + + expect(result.hasAccess).toBe(false) + expect(result.canAdmin).toBe(false) + }) + + it('does not elevate org admins on a workspace with no organization', async () => { + mockSelectSequence([[{ id: 'ws', ownerId: 'other-user', organizationId: null }], []]) + + const result = await checkWorkspaceAccess('ws', 'some-user') + + expect(result.hasAccess).toBe(false) + }) + }) }) diff --git a/apps/sim/lib/workspaces/permissions/utils.ts b/apps/sim/lib/workspaces/permissions/utils.ts index c2db39c7135..b815a69be86 100644 --- a/apps/sim/lib/workspaces/permissions/utils.ts +++ b/apps/sim/lib/workspaces/permissions/utils.ts @@ -1,16 +1,18 @@ import { db } from '@sim/db' +import { member, permissions, user, type WorkspaceMode, workspace } from '@sim/db/schema' import { - member, - permissions, - type permissionTypeEnum, - user, - type WorkspaceMode, - workspace, -} from '@sim/db/schema' -import { and, eq, isNull } from 'drizzle-orm' + isOrgAdminRole, + ORG_ADMIN_ROLES, + PERMISSION_RANK, + type PermissionType, + permissionSatisfies, + resolveEffectiveWorkspacePermission, +} from '@sim/platform-authz/workspace' +import { and, eq, inArray, isNull } from 'drizzle-orm' import { HttpError } from '@/lib/core/utils/http-error' +import { getOrgAdminWorkspaceRows } from '@/lib/workspaces/utils' -export type PermissionType = (typeof permissionTypeEnum.enumValues)[number] +export type { PermissionType } export interface WorkspaceBasic { id: string } @@ -29,6 +31,7 @@ export interface WorkspaceAccess { exists: boolean hasAccess: boolean canWrite: boolean + canAdmin: boolean workspace: WorkspaceWithOwner | null } @@ -102,11 +105,32 @@ export async function getWorkspaceWithOwner( return ws || null } +/** + * Resolve the effective workspace permission for a user under the governance + * inheritance model: the owners/admins of the organization that owns the + * workspace are derived workspace admins. Returns the higher of any explicit + * grant and the org-admin derivation. The workspace owner is not special-cased — + * they always hold an explicit `admin` row, so the resolver's lookup covers them. + * + * Delegates to the shared resolver in `@sim/platform-authz/workspace` so the + * rule has a single source of truth shared with the realtime server. + * + * @param userId - The user to resolve the permission for + * @param ws - The workspace (organization already loaded) + */ +export async function getEffectiveWorkspacePermission( + userId: string, + ws: Pick +): Promise { + return resolveEffectiveWorkspacePermission(userId, ws.id, ws.organizationId) +} + /** * Check workspace access for a user * * Verifies the workspace exists and the user has access to it. - * Returns access level (read/write) based on ownership and permissions. + * Returns access level (read/write) based on ownership, explicit permissions, + * and organization-admin inheritance. * * @param workspaceId - The workspace ID to check * @param userId - The user ID to check access for @@ -119,33 +143,15 @@ export async function checkWorkspaceAccess( const ws = await getWorkspaceWithOwner(workspaceId) if (!ws) { - return { exists: false, hasAccess: false, canWrite: false, workspace: null } - } - - if (ws.ownerId === userId) { - return { exists: true, hasAccess: true, canWrite: true, workspace: ws } - } - - const [permissionRow] = await db - .select({ permissionType: permissions.permissionType }) - .from(permissions) - .where( - and( - eq(permissions.userId, userId), - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspaceId) - ) - ) - .limit(1) - - if (!permissionRow) { - return { exists: true, hasAccess: false, canWrite: false, workspace: ws } + return { exists: false, hasAccess: false, canWrite: false, canAdmin: false, workspace: null } } - const canWrite = - permissionRow.permissionType === 'write' || permissionRow.permissionType === 'admin' + const permission = await getEffectiveWorkspacePermission(userId, ws) + const hasAccess = permission !== null + const canWrite = permissionSatisfies(permission, 'write') + const canAdmin = permissionSatisfies(permission, 'admin') - return { exists: true, hasAccess: true, canWrite, workspace: ws } + return { exists: true, hasAccess, canWrite, canAdmin, workspace: ws } } /** @@ -194,10 +200,11 @@ export async function getUserEntityPermissions( entityId: string ): Promise { if (entityType === 'workspace') { - const activeWorkspace = await workspaceExists(entityId) - if (!activeWorkspace) { + const ws = await getWorkspaceWithOwner(entityId) + if (!ws) { return null } + return getEffectiveWorkspacePermission(userId, ws) } const result = await db @@ -215,9 +222,8 @@ export async function getUserEntityPermissions( return null } - const permissionOrder: Record = { admin: 3, write: 2, read: 1 } const highestPermission = result.reduce((highest, current) => { - return permissionOrder[current.permissionType] > permissionOrder[highest.permissionType] + return PERMISSION_RANK[current.permissionType] > PERMISSION_RANK[highest.permissionType] ? current : highest }) @@ -225,30 +231,6 @@ export async function getUserEntityPermissions( return highestPermission.permissionType } -/** - * Check if a user has admin permission for a specific workspace - * - * @param userId - The ID of the user to check - * @param workspaceId - The ID of the workspace to check - * @returns Promise - True if the user has admin permission for the workspace, false otherwise - */ -export async function hasAdminPermission(userId: string, workspaceId: string): Promise { - const result = await db - .select({ id: permissions.id }) - .from(permissions) - .where( - and( - eq(permissions.userId, userId), - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspaceId), - eq(permissions.permissionType, 'admin') - ) - ) - .limit(1) - - return result.length > 0 -} - /** * Retrieves a list of users with their associated permissions for a given workspace. * @@ -261,18 +243,30 @@ export async function hasAdminPermission(userId: string, workspaceId: string): P * @param workspaceId - The ID of the workspace to retrieve user permissions for. * @returns A promise that resolves to an array of user objects, each containing user details and their permission type. */ -export async function getUsersWithPermissions(workspaceId: string): Promise< - Array<{ - userId: string - email: string - name: string - image: string | null - permissionType: PermissionType - isExternal: boolean - joinedAt: string - }> -> { - const usersWithPermissions = await db +export type MemberRoleSource = 'owner' | 'explicit' | 'org-admin' + +export interface WorkspaceMemberWithRole { + userId: string + email: string + name: string + image: string | null + permissionType: PermissionType + isExternal: boolean + joinedAt: string + /** + * Where the effective role comes from. `org-admin` and `owner` roles are + * derived and cannot be changed through the member UI. + */ + roleSource: MemberRoleSource +} + +export async function getUsersWithPermissions( + workspaceId: string +): Promise { + const ws = await getWorkspaceWithOwner(workspaceId) + if (!ws) return [] + + const explicitRows = await db .select({ userId: user.id, email: user.email, @@ -280,33 +274,72 @@ export async function getUsersWithPermissions(workspaceId: string): Promise< image: user.image, permissionType: permissions.permissionType, joinedAt: permissions.createdAt, - workspaceOrganizationId: workspace.organizationId, - workspaceOwnerId: workspace.ownerId, userOrganizationId: member.organizationId, }) .from(permissions) .innerJoin(user, eq(permissions.userId, user.id)) - .innerJoin(workspace, eq(permissions.entityId, workspace.id)) .leftJoin(member, eq(member.userId, user.id)) - .where( - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspaceId), - isNull(workspace.archivedAt) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))) + + const byUser = new Map() + + for (const row of explicitRows) { + const isOwner = row.userId === ws.ownerId + byUser.set(row.userId, { + userId: row.userId, + email: row.email, + name: row.name, + image: row.image ?? null, + permissionType: row.permissionType, + isExternal: !isOwner && row.userOrganizationId !== ws.organizationId, + joinedAt: row.joinedAt.toISOString(), + roleSource: isOwner ? 'owner' : 'explicit', + }) + } + + if (ws.organizationId) { + const orgAdmins = await db + .select({ + userId: user.id, + email: user.email, + name: user.name, + image: user.image, + joinedAt: member.createdAt, + }) + .from(member) + .innerJoin(user, eq(member.userId, user.id)) + .where( + and( + eq(member.organizationId, ws.organizationId), + inArray(member.role, [...ORG_ADMIN_ROLES]) + ) ) - ) - .orderBy(user.email) - - return usersWithPermissions.map((row) => ({ - userId: row.userId, - email: row.email, - name: row.name, - image: row.image ?? null, - permissionType: row.permissionType, - isExternal: - row.userId !== row.workspaceOwnerId && row.userOrganizationId !== row.workspaceOrganizationId, - joinedAt: row.joinedAt.toISOString(), - })) + + for (const row of orgAdmins) { + const isOwner = row.userId === ws.ownerId + const existing = byUser.get(row.userId) + if (existing) { + existing.permissionType = 'admin' + existing.isExternal = false + if (existing.roleSource !== 'owner') { + existing.roleSource = isOwner ? 'owner' : 'org-admin' + } + } else { + byUser.set(row.userId, { + userId: row.userId, + email: row.email, + name: row.name, + image: row.image ?? null, + permissionType: 'admin', + isExternal: false, + joinedAt: row.joinedAt.toISOString(), + roleSource: isOwner ? 'owner' : 'org-admin', + }) + } + } + } + + return Array.from(byUser.values()).sort((a, b) => a.email.localeCompare(b.email)) } /** Lightweight profile data for workspace member display (avatars, owner cells). */ @@ -360,15 +393,7 @@ export async function hasWorkspaceAdminAccess( return false } - if (ws.ownerId === userId) { - return true - } - - if (await hasAdminPermission(userId, workspaceId)) { - return true - } - - return await isOrganizationAdminOrOwnerOfWorkspace(userId, ws) + return (await getEffectiveWorkspacePermission(userId, ws)) === 'admin' } /** @@ -387,7 +412,7 @@ export async function isOrganizationAdminOrOwner( .from(member) .where(and(eq(member.userId, userId), eq(member.organizationId, organizationId))) .limit(1) - return row?.role === 'owner' || row?.role === 'admin' + return isOrgAdminRole(row?.role) } /** @@ -409,14 +434,6 @@ export async function isOrganizationMember( return !!row } -async function isOrganizationAdminOrOwnerOfWorkspace( - userId: string, - ws: Pick -): Promise { - if (!ws.organizationId) return false - return isOrganizationAdminOrOwner(userId, ws.organizationId) -} - /** * Get a list of workspaces that the user has access to * @@ -462,13 +479,26 @@ export async function getManageableWorkspaces(userId: string): Promise< ) ) + const orgAdminWorkspaces = (await getOrgAdminWorkspaceRows(userId, 'active')).map((ws) => ({ + id: ws.id, + name: ws.name, + ownerId: ws.ownerId, + })) + const ownedSet = new Set(ownedWorkspaces.map((w) => w.id)) - const combined = [ - ...ownedWorkspaces.map((ws) => ({ ...ws, accessType: 'owner' as const })), - ...adminWorkspaces - .filter((ws) => !ownedSet.has(ws.id)) - .map((ws) => ({ ...ws, accessType: 'direct' as const })), - ] + const seen = new Set(ownedSet) + const combined: Array<{ + id: string + name: string + ownerId: string + accessType: 'direct' | 'owner' + }> = ownedWorkspaces.map((ws) => ({ ...ws, accessType: 'owner' as const })) + + for (const ws of [...adminWorkspaces, ...orgAdminWorkspaces]) { + if (seen.has(ws.id)) continue + seen.add(ws.id) + combined.push({ ...ws, accessType: 'direct' as const }) + } return combined } diff --git a/apps/sim/lib/workspaces/policy.ts b/apps/sim/lib/workspaces/policy.ts index 641dd15e9af..addb1fcad10 100644 --- a/apps/sim/lib/workspaces/policy.ts +++ b/apps/sim/lib/workspaces/policy.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { member, type WorkspaceMode, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, count, eq, isNull } from 'drizzle-orm' import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' @@ -249,7 +250,7 @@ export async function getWorkspaceCreationPolicy({ if (organizationId && orgRole) { const billedAccountUserId = await requireOrganizationOwnerId(organizationId) - if (!['owner', 'admin'].includes(orgRole)) { + if (!isOrgAdminRole(orgRole)) { return { canCreate: false, workspaceMode: WORKSPACE_MODE.ORGANIZATION, @@ -298,7 +299,7 @@ export async function getWorkspaceCreationPolicy({ ) { const billedAccountUserId = await requireOrganizationOwnerId(organizationId) - if (!['owner', 'admin'].includes(orgRole)) { + if (!isOrgAdminRole(orgRole)) { return { canCreate: false, workspaceMode: WORKSPACE_MODE.ORGANIZATION, diff --git a/apps/sim/lib/workspaces/utils.test.ts b/apps/sim/lib/workspaces/utils.test.ts index 00227a9d795..d540670dc70 100644 --- a/apps/sim/lib/workspaces/utils.test.ts +++ b/apps/sim/lib/workspaces/utils.test.ts @@ -1,13 +1,30 @@ /** * @vitest-environment node */ +import { db } from '@sim/db' import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/db', () => ({ - db: {}, + db: { select: vi.fn() }, })) -import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx } from '@/lib/workspaces/utils' +import { + listAccessibleWorkspaceRowsForUser, + reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, +} from '@/lib/workspaces/utils' + +const mockDb = db as unknown as { select: ReturnType } + +function createMockChain(finalResult: unknown) { + const chain: any = {} + chain.then = vi.fn().mockImplementation((resolve: any) => resolve(finalResult)) + chain.from = vi.fn().mockReturnValue(chain) + chain.where = vi.fn().mockReturnValue(chain) + chain.innerJoin = vi.fn().mockReturnValue(chain) + chain.limit = vi.fn().mockReturnValue(chain) + chain.orderBy = vi.fn().mockReturnValue(chain) + return chain +} function createSelectChain(result: unknown) { const limit = vi.fn().mockResolvedValue(result) @@ -132,3 +149,46 @@ describe('reassignWorkflowOwnershipForWorkspaceMemberRemovalTx', () => { expect(result).toEqual({ reassigned: [], unresolved: ['workspace-1'] }) }) }) + +describe('listAccessibleWorkspaceRowsForUser', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('elevates an org admin to admin on an org workspace where they hold a lower explicit grant', async () => { + const orgWorkspace = { id: 'ws-1', name: 'Shared', ownerId: 'owner-x', organizationId: 'org-1' } + + mockDb.select + .mockReturnValueOnce(createMockChain([{ workspace: orgWorkspace, permissionType: 'write' }])) + .mockReturnValueOnce(createMockChain([{ organizationId: 'org-1', role: 'admin' }])) + .mockReturnValueOnce(createMockChain([orgWorkspace])) + + const rows = await listAccessibleWorkspaceRowsForUser('user-1', 'active') + + expect(rows).toEqual([{ workspace: orgWorkspace, permissionType: 'admin' }]) + }) + + it('keeps a lower explicit grant on a workspace owned by a different organization', async () => { + const externalWorkspace = { + id: 'ws-ext', + name: 'External', + ownerId: 'owner-y', + organizationId: 'org-2', + } + const orgWorkspace = { id: 'ws-1', name: 'Shared', ownerId: 'owner-x', organizationId: 'org-1' } + + mockDb.select + .mockReturnValueOnce( + createMockChain([{ workspace: externalWorkspace, permissionType: 'write' }]) + ) + .mockReturnValueOnce(createMockChain([{ organizationId: 'org-1', role: 'admin' }])) + .mockReturnValueOnce(createMockChain([orgWorkspace])) + + const rows = await listAccessibleWorkspaceRowsForUser('user-1', 'active') + + expect(rows).toEqual([ + { workspace: externalWorkspace, permissionType: 'write' }, + { workspace: orgWorkspace, permissionType: 'admin' }, + ]) + }) +}) diff --git a/apps/sim/lib/workspaces/utils.ts b/apps/sim/lib/workspaces/utils.ts index 88ea402ecdf..4c65d5e5f35 100644 --- a/apps/sim/lib/workspaces/utils.ts +++ b/apps/sim/lib/workspaces/utils.ts @@ -1,6 +1,8 @@ import { db } from '@sim/db' -import { permissions, workflow, workspace as workspaceTable } from '@sim/db/schema' +import { member, permissions, workflow, workspace as workspaceTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import type { PermissionType } from '@sim/platform-authz/workspace' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { generateId } from '@sim/utils/id' import { and, count, desc, eq, inArray, isNull, ne, sql } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' @@ -45,14 +47,48 @@ export async function getWorkspaceBilledAccountUserId(workspaceId: string): Prom return settings?.billedAccountUserId ?? null } -export async function listUserWorkspaces(userId: string, scope: WorkspaceScope = 'active') { - const workspaces = await db - .select({ - workspaceId: workspaceTable.id, - workspaceName: workspaceTable.name, - ownerId: workspaceTable.ownerId, - permissionType: permissions.permissionType, - }) +/** + * Workspaces the user administers purely through organization owner/admin role, + * with no explicit permission row required. Empty when the user is not an org + * owner/admin. Implements the workspace-permission inheritance model. + */ +export async function getOrgAdminWorkspaceRows( + userId: string, + scope: WorkspaceScope = 'active' +): Promise> { + const [membership] = await db + .select({ organizationId: member.organizationId, role: member.role }) + .from(member) + .where(eq(member.userId, userId)) + .limit(1) + + if (!membership || !isOrgAdminRole(membership.role)) { + return [] + } + + const orgFilter = eq(workspaceTable.organizationId, membership.organizationId) + const where = + scope === 'all' + ? orgFilter + : scope === 'archived' + ? and(orgFilter, sql`${workspaceTable.archivedAt} IS NOT NULL`) + : and(orgFilter, isNull(workspaceTable.archivedAt)) + + return db.select().from(workspaceTable).where(where).orderBy(desc(workspaceTable.createdAt)) +} + +/** + * Every workspace a user can access: explicit permission grants plus workspaces + * derived from organization owner/admin role. Deduped with explicit rows first. + */ +export async function listAccessibleWorkspaceRowsForUser( + userId: string, + scope: WorkspaceScope = 'active' +): Promise< + Array<{ workspace: typeof workspaceTable.$inferSelect; permissionType: PermissionType }> +> { + const explicit = await db + .select({ workspace: workspaceTable, permissionType: permissions.permissionType }) .from(permissions) .innerJoin(workspaceTable, eq(permissions.entityId, workspaceTable.id)) .where( @@ -72,10 +108,31 @@ export async function listUserWorkspaces(userId: string, scope: WorkspaceScope = ) .orderBy(desc(workspaceTable.createdAt)) - return workspaces.map((row) => ({ - workspaceId: row.workspaceId, - workspaceName: row.workspaceName, - role: row.ownerId === userId ? 'owner' : row.permissionType, + const orgRows = await getOrgAdminWorkspaceRows(userId, scope) + if (orgRows.length === 0) { + return explicit + } + + const orgWorkspaceIds = new Set(orgRows.map((ws) => ws.id)) + const seen = new Set(explicit.map((row) => row.workspace.id)) + + const elevatedExplicit = explicit.map((row) => + orgWorkspaceIds.has(row.workspace.id) ? { ...row, permissionType: 'admin' as const } : row + ) + const derived = orgRows + .filter((ws) => !seen.has(ws.id)) + .map((ws) => ({ workspace: ws, permissionType: 'admin' as const })) + + return [...elevatedExplicit, ...derived] +} + +export async function listUserWorkspaces(userId: string, scope: WorkspaceScope = 'active') { + const rows = await listAccessibleWorkspaceRowsForUser(userId, scope) + + return rows.map(({ workspace: ws, permissionType }) => ({ + workspaceId: ws.id, + workspaceName: ws.name, + role: ws.ownerId === userId ? 'owner' : permissionType, })) } @@ -349,3 +406,102 @@ export async function reassignBilledAccountForUser( return { reassigned, unresolved } } + +export interface ReassignOwnedWorkspacesResult { + reassigned: Array<{ workspaceId: string; newOwnerId: string }> + unresolved: string[] +} + +/** + * Reassigns `ownerId` on every workspace owned by `departingUserId` to another + * eligible user, so the user can be deleted without the `workspace.owner_id` + * `ON DELETE CASCADE` silently deleting their workspaces. + * + * Preference order for the replacement: + * 1. The workspace billed account (if different from the departing user) + * 2. Any other workspace admin + * + * Returns workspaces that could not be reassigned (no distinct billed account and + * no other admin). Callers MUST block user deletion when `unresolved.length > 0` + * so the cascade can never nuke a workspace. + */ +export async function reassignOwnedWorkspacesForUser( + departingUserId: string +): Promise { + const ownedWorkspaces = await db + .select({ + id: workspaceTable.id, + billedAccountUserId: workspaceTable.billedAccountUserId, + }) + .from(workspaceTable) + .where(eq(workspaceTable.ownerId, departingUserId)) + + if (ownedWorkspaces.length === 0) { + return { reassigned: [], unresolved: [] } + } + + const reassigned: ReassignOwnedWorkspacesResult['reassigned'] = [] + const unresolved: string[] = [] + + for (const ws of ownedWorkspaces) { + let replacement: string | null = + ws.billedAccountUserId !== departingUserId ? ws.billedAccountUserId : null + + if (!replacement) { + const [admin] = await db + .select({ userId: permissions.userId }) + .from(permissions) + .where( + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, ws.id), + eq(permissions.permissionType, 'admin'), + ne(permissions.userId, departingUserId) + ) + ) + .limit(1) + + replacement = admin?.userId ?? null + } + + if (!replacement) { + unresolved.push(ws.id) + continue + } + + const now = new Date() + await db + .update(workspaceTable) + .set({ ownerId: replacement, updatedAt: now }) + .where(eq(workspaceTable.id, ws.id)) + + // Owners are admins — guarantee the new owner holds an admin permission row. + await db + .insert(permissions) + .values({ + id: generateId(), + userId: replacement, + entityType: 'workspace', + entityId: ws.id, + permissionType: 'admin', + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [permissions.userId, permissions.entityType, permissions.entityId], + set: { permissionType: 'admin', updatedAt: now }, + }) + + reassigned.push({ workspaceId: ws.id, newOwnerId: replacement }) + } + + if (reassigned.length > 0) { + logger.info('Reassigned workspace ownership for departing user', { + departingUserId, + reassignedCount: reassigned.length, + unresolvedCount: unresolved.length, + }) + } + + return { reassigned, unresolved } +} diff --git a/apps/sim/package.json b/apps/sim/package.json index be49eca24d7..c90dc98996b 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -95,10 +95,10 @@ "@react-email/render": "2.0.8", "@sim/audit": "workspace:*", "@sim/logger": "workspace:*", + "@sim/platform-authz": "workspace:*", "@sim/realtime-protocol": "workspace:*", "@sim/security": "workspace:*", "@sim/utils": "workspace:*", - "@sim/workflow-authz": "workspace:*", "@sim/workflow-persistence": "workspace:*", "@sim/workflow-types": "workspace:*", "@t3-oss/env-nextjs": "0.13.4", diff --git a/apps/sim/vitest.setup.ts b/apps/sim/vitest.setup.ts index b5b7aa72952..92e945dcca0 100644 --- a/apps/sim/vitest.setup.ts +++ b/apps/sim/vitest.setup.ts @@ -21,7 +21,7 @@ vi.mock('@sim/db', () => databaseMock) vi.mock('@sim/db/schema', () => schemaMock) vi.mock('drizzle-orm', () => drizzleOrmMock) vi.mock('@sim/logger', () => loggerMock) -vi.mock('@sim/workflow-authz', () => workflowAuthzMock) +vi.mock('@sim/platform-authz/workflow', () => workflowAuthzMock) vi.mock('@/lib/auth', () => authMock) vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) vi.mock('@/lib/core/utils/request', () => requestUtilsMock) diff --git a/bun.lock b/bun.lock index ad58c9cb044..fb7f52d232e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -62,10 +61,10 @@ "@sim/auth": "workspace:*", "@sim/db": "workspace:*", "@sim/logger": "workspace:*", + "@sim/platform-authz": "workspace:*", "@sim/realtime-protocol": "workspace:*", "@sim/security": "workspace:*", "@sim/utils": "workspace:*", - "@sim/workflow-authz": "workspace:*", "@sim/workflow-persistence": "workspace:*", "@sim/workflow-types": "workspace:*", "@socket.io/redis-adapter": "8.3.0", @@ -151,10 +150,10 @@ "@react-email/render": "2.0.8", "@sim/audit": "workspace:*", "@sim/logger": "workspace:*", + "@sim/platform-authz": "workspace:*", "@sim/realtime-protocol": "workspace:*", "@sim/security": "workspace:*", "@sim/utils": "workspace:*", - "@sim/workflow-authz": "workspace:*", "@sim/workflow-persistence": "workspace:*", "@sim/workflow-types": "workspace:*", "@t3-oss/env-nextjs": "0.13.4", @@ -360,6 +359,18 @@ "vitest": "^4.1.0", }, }, + "packages/platform-authz": { + "name": "@sim/platform-authz", + "version": "0.1.0", + "dependencies": { + "@sim/db": "workspace:*", + "drizzle-orm": "^0.45.2", + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, "packages/realtime-protocol": { "name": "@sim/realtime-protocol", "version": "0.1.0", @@ -423,18 +434,6 @@ "vitest": "^4.1.0", }, }, - "packages/workflow-authz": { - "name": "@sim/workflow-authz", - "version": "0.1.0", - "dependencies": { - "@sim/db": "workspace:*", - "drizzle-orm": "^0.45.2", - }, - "devDependencies": { - "@sim/tsconfig": "workspace:*", - "typescript": "^5.7.3", - }, - }, "packages/workflow-persistence": { "name": "@sim/workflow-persistence", "version": "0.1.0", @@ -1410,6 +1409,8 @@ "@sim/logger": ["@sim/logger@workspace:packages/logger"], + "@sim/platform-authz": ["@sim/platform-authz@workspace:packages/platform-authz"], + "@sim/realtime": ["@sim/realtime@workspace:apps/realtime"], "@sim/realtime-protocol": ["@sim/realtime-protocol@workspace:packages/realtime-protocol"], @@ -1422,8 +1423,6 @@ "@sim/utils": ["@sim/utils@workspace:packages/utils"], - "@sim/workflow-authz": ["@sim/workflow-authz@workspace:packages/workflow-authz"], - "@sim/workflow-persistence": ["@sim/workflow-persistence@workspace:packages/workflow-persistence"], "@sim/workflow-types": ["@sim/workflow-types@workspace:packages/workflow-types"], diff --git a/packages/db/migrations/0243_kb_workspace_cascade.sql b/packages/db/migrations/0243_kb_workspace_cascade.sql new file mode 100644 index 00000000000..b46d423fde8 --- /dev/null +++ b/packages/db/migrations/0243_kb_workspace_cascade.sql @@ -0,0 +1,6 @@ +-- migration-safe: re-creates the existing knowledge_base→workspace FK only to change its ON DELETE action to cascade. The column and FK are otherwise unchanged and the FK is re-added immediately below (atomic within the migration transaction); no app code depends on the FK's delete action. +ALTER TABLE "knowledge_base" DROP CONSTRAINT "knowledge_base_workspace_id_workspace_id_fk"; +--> statement-breakpoint +ALTER TABLE "knowledge_base" ADD CONSTRAINT "knowledge_base_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action NOT VALID; +--> statement-breakpoint +ALTER TABLE "knowledge_base" VALIDATE CONSTRAINT "knowledge_base_workspace_id_workspace_id_fk"; diff --git a/packages/db/migrations/meta/0243_snapshot.json b/packages/db/migrations/meta/0243_snapshot.json new file mode 100644 index 00000000000..5b30f600ded --- /dev/null +++ b/packages/db/migrations/meta/0243_snapshot.json @@ -0,0 +1,16729 @@ +{ + "id": "dca0a2e7-f031-462f-bda2-d6f52ab4a9ff", + "prevId": "78b8f3d4-c24c-4303-89ec-8ae29d2ea5c8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 769b282f8b4..2dabe616501 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1695,6 +1695,13 @@ "when": 1781818772450, "tag": "0242_public_share", "breakpoints": true + }, + { + "idx": 243, + "version": "7", + "when": 1781895339512, + "tag": "0243_kb_workspace_cascade", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 54366a6a024..f82fb8b2397 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1231,6 +1231,16 @@ export const workspace = pgTable( name: text('name').notNull(), color: text('color').notNull().default('#33C482'), logoUrl: text('logo_url'), + /** + * @deprecated Not a permission or identity concept — do not use for admin/access + * checks. The owner→admin derivation is redundant: every workspace owner already + * has an explicit `admin` row in `permissions` (verified across all production + * workspaces) and all creation paths add one. Retained only as the lifecycle + * anchor — `onDelete: 'cascade'` cleans up a user's workspaces on account + * deletion — and the ownership-transfer target when an owner is removed. For + * admin checks use explicit `permissions` rows; for the workspace's principal + * billing identity use `billedAccountUserId`. + */ ownerId: text('owner_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), @@ -1447,6 +1457,17 @@ export const invitationWorkspaceGrant = pgTable( }) ) +/** + * Polymorphic access grants: `entityType` + `entityId` reference a workspace, + * workflow, organization, etc. by id, but `entityId` is **not a foreign key** — + * so deleting the referenced entity does NOT cascade-delete these rows. Soft + * deletes (e.g. workspace archive) intentionally keep them: the entity is blocked + * everywhere by its `archivedAt`, so the rows are harmless, and a future restore + * would need them. Only a **hard** delete/purge of an entity must remove its + * grants explicitly — e.g. + * `DELETE FROM permissions WHERE entity_type = 'workspace' AND entity_id = $id` — + * or they orphan. + */ export const permissions = pgTable( 'permissions', { @@ -1528,7 +1549,7 @@ export const knowledgeBase = pgTable( userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), - workspaceId: text('workspace_id').references(() => workspace.id), + workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), name: text('name').notNull(), description: text('description'), diff --git a/packages/workflow-authz/package.json b/packages/platform-authz/package.json similarity index 63% rename from packages/workflow-authz/package.json rename to packages/platform-authz/package.json index 8bfd7b9ebe2..adebe6563fa 100644 --- a/packages/workflow-authz/package.json +++ b/packages/platform-authz/package.json @@ -1,5 +1,5 @@ { - "name": "@sim/workflow-authz", + "name": "@sim/platform-authz", "version": "0.1.0", "private": true, "sideEffects": false, @@ -10,9 +10,17 @@ "node": ">=20.0.0" }, "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" + "./predicates": { + "types": "./src/predicates.ts", + "default": "./src/predicates.ts" + }, + "./workspace": { + "types": "./src/workspace.ts", + "default": "./src/workspace.ts" + }, + "./workflow": { + "types": "./src/workflow.ts", + "default": "./src/workflow.ts" } }, "scripts": { diff --git a/packages/platform-authz/src/predicates.ts b/packages/platform-authz/src/predicates.ts new file mode 100644 index 00000000000..ff1afa6e11d --- /dev/null +++ b/packages/platform-authz/src/predicates.ts @@ -0,0 +1,37 @@ +import type { permissionTypeEnum } from '@sim/db/schema' + +/** Workspace permission level: read < write < admin. */ +export type PermissionType = (typeof permissionTypeEnum.enumValues)[number] + +/** Total ordering of workspace permission levels: read < write < admin. */ +export const PERMISSION_RANK = { read: 1, write: 2, admin: 3 } as const satisfies Record< + PermissionType, + number +> + +/** + * Whether an effective permission satisfies a required level under the + * read < write < admin ordering. `null`/`undefined` (no access) never satisfies. + * Single source of truth for permission-level comparisons across the app and the + * realtime server — replaces the hand-written `=== 'admin' || === 'write'` ladders. + */ +export function permissionSatisfies( + have: PermissionType | null | undefined, + required: PermissionType +): boolean { + return have != null && PERMISSION_RANK[have] >= PERMISSION_RANK[required] +} + +/** Organization membership roles (Better Auth) that confer admin authority. */ +export const ORG_ADMIN_ROLES = ['owner', 'admin'] as const + +/** + * Whether an organization membership role is owner/admin. Owner/admin org roles + * are derived workspace admins on the org's workspaces — single source of truth + * for the `role === 'owner' || role === 'admin'` predicate, shared by server + * resolvers and client UIs. Dependency-free (the only import is a type, which is + * erased) so client bundles can import it without pulling in the DB client. + */ +export function isOrgAdminRole(role: string | null | undefined): boolean { + return role === 'owner' || role === 'admin' +} diff --git a/packages/workflow-authz/src/index.ts b/packages/platform-authz/src/workflow.ts similarity index 86% rename from packages/workflow-authz/src/index.ts rename to packages/platform-authz/src/workflow.ts index 5eef77f8e08..c1d643a5850 100644 --- a/packages/workflow-authz/src/index.ts +++ b/packages/platform-authz/src/workflow.ts @@ -1,18 +1,19 @@ -import { - db, - permissions, - type permissionTypeEnum, - workflow, - workflowFolder, - workspace, -} from '@sim/db' +import { db, workflow, workflowFolder, workspace } from '@sim/db' import { and, eq, isNull } from 'drizzle-orm' +import { + type PermissionType, + permissionSatisfies, + resolveEffectiveWorkspacePermission, +} from './workspace' + +export type { PermissionType } export type ActiveWorkflowRecord = typeof workflow.$inferSelect export interface ActiveWorkflowContext { workflow: ActiveWorkflowRecord workspaceId: string + workspaceOrganizationId: string | null } export async function getActiveWorkflowContext( @@ -22,6 +23,7 @@ export async function getActiveWorkflowContext( .select({ workflow, workspaceId: workspace.id, + workspaceOrganizationId: workspace.organizationId, }) .from(workflow) .innerJoin(workspace, eq(workflow.workspaceId, workspace.id)) @@ -37,6 +39,7 @@ export async function getActiveWorkflowContext( return { workflow: rows[0].workflow, workspaceId: rows[0].workspaceId, + workspaceOrganizationId: rows[0].workspaceOrganizationId, } } @@ -57,8 +60,6 @@ export async function assertActiveWorkflowContext( return context } -export type PermissionType = (typeof permissionTypeEnum.enumValues)[number] - type WorkflowRecord = typeof workflow.$inferSelect export class WorkflowLockedError extends Error { @@ -276,38 +277,13 @@ export async function authorizeWorkflowByWorkspacePermission(params: { } } - const [permissionRow] = await db - .select({ permissionType: permissions.permissionType }) - .from(permissions) - .where( - and( - eq(permissions.userId, userId), - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, wf.workspaceId) - ) - ) - .limit(1) - - const workspacePermission = (permissionRow?.permissionType as PermissionType | undefined) ?? null - - if (workspacePermission === null) { - return { - allowed: false, - status: 403, - message: `Unauthorized: Access denied to ${action} this workflow`, - workflow: wf, - workspacePermission, - } - } - - const permissionSatisfied = - action === 'read' - ? true - : action === 'write' - ? workspacePermission === 'write' || workspacePermission === 'admin' - : workspacePermission === 'admin' + const workspacePermission = await resolveEffectiveWorkspacePermission( + userId, + wf.workspaceId, + activeContext.workspaceOrganizationId + ) - if (!permissionSatisfied) { + if (!permissionSatisfies(workspacePermission, action)) { return { allowed: false, status: 403, diff --git a/packages/platform-authz/src/workspace.ts b/packages/platform-authz/src/workspace.ts new file mode 100644 index 00000000000..775356d754e --- /dev/null +++ b/packages/platform-authz/src/workspace.ts @@ -0,0 +1,55 @@ +import { db } from '@sim/db' +import { member, permissions } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import { isOrgAdminRole, type PermissionType } from './predicates' + +export * from './predicates' + +/** + * Resolves the effective workspace permission under the governance inheritance + * model: the owners/admins of the organization that owns the workspace are + * derived workspace admins. Returns the higher of any explicit grant and the + * org-admin derivation. + * + * The workspace owner is intentionally NOT a special case: every owner already + * holds an explicit `admin` row in `permissions` (added at creation, verified + * across all production workspaces), so the lookup below already grants them + * admin. `workspace.ownerId` is a lifecycle anchor, not a permission input. + * + * Single source of truth for workspace-permission resolution, shared by the Next + * app (`getEffectiveWorkspacePermission`) and the realtime server (via the + * `/workflow` entry). Lives in a package because `apps/realtime` needs it and + * packages may not import app code. + */ +export async function resolveEffectiveWorkspacePermission( + userId: string, + workspaceId: string, + workspaceOrganizationId: string | null +): Promise { + const [permissionRow] = await db + .select({ permissionType: permissions.permissionType }) + .from(permissions) + .where( + and( + eq(permissions.userId, userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .limit(1) + + const explicit = (permissionRow?.permissionType as PermissionType | undefined) ?? null + + if (workspaceOrganizationId && explicit !== 'admin') { + const [memberRow] = await db + .select({ role: member.role }) + .from(member) + .where(and(eq(member.userId, userId), eq(member.organizationId, workspaceOrganizationId))) + .limit(1) + if (isOrgAdminRole(memberRow?.role)) { + return 'admin' + } + } + + return explicit +} diff --git a/packages/workflow-authz/tsconfig.json b/packages/platform-authz/tsconfig.json similarity index 100% rename from packages/workflow-authz/tsconfig.json rename to packages/platform-authz/tsconfig.json diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index 7bff8617850..5ddbd73aa95 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -137,7 +137,7 @@ export { } from './terminal-console.mock' // URL mocks export { urlsMock, urlsMockFns } from './urls.mock' -// Workflow authz package mocks (for @sim/workflow-authz) +// Workflow authz package mocks (for @sim/platform-authz/workflow) export { workflowAuthzMock, workflowAuthzMockFns } from './workflow-authz.mock' // Workflows API utils mocks (for @/app/api/workflows/utils) export { workflowsApiUtilsMock, workflowsApiUtilsMockFns } from './workflows-api-utils.mock' diff --git a/packages/testing/src/mocks/permissions.mock.ts b/packages/testing/src/mocks/permissions.mock.ts index 167d079f9a8..9b38a9da1f7 100644 --- a/packages/testing/src/mocks/permissions.mock.ts +++ b/packages/testing/src/mocks/permissions.mock.ts @@ -19,7 +19,6 @@ export const permissionsMockFns = { mockCheckWorkspaceAccess: vi.fn(), mockAssertActiveWorkspaceAccess: vi.fn(), mockGetUserEntityPermissions: vi.fn(), - mockHasAdminPermission: vi.fn(), mockGetUsersWithPermissions: vi.fn(), mockGetWorkspaceMemberProfiles: vi.fn(), mockHasWorkspaceAdminAccess: vi.fn(), @@ -42,7 +41,6 @@ export const permissionsMock = { checkWorkspaceAccess: permissionsMockFns.mockCheckWorkspaceAccess, assertActiveWorkspaceAccess: permissionsMockFns.mockAssertActiveWorkspaceAccess, getUserEntityPermissions: permissionsMockFns.mockGetUserEntityPermissions, - hasAdminPermission: permissionsMockFns.mockHasAdminPermission, getUsersWithPermissions: permissionsMockFns.mockGetUsersWithPermissions, getWorkspaceMemberProfiles: permissionsMockFns.mockGetWorkspaceMemberProfiles, hasWorkspaceAdminAccess: permissionsMockFns.mockHasWorkspaceAdminAccess, diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index fb72acb5b1c..0fb21283072 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -941,7 +941,9 @@ export const schemaMock = { executionId: 'executionId', createdAt: 'createdAt', }, - credentialTypeEnum: 'credentialTypeEnum', + credentialTypeEnum: { + enumValues: ['oauth', 'env_workspace', 'env_personal', 'service_account'] as const, + }, credential: { id: 'id', workspaceId: 'workspaceId', diff --git a/packages/testing/src/mocks/workflow-authz.mock.ts b/packages/testing/src/mocks/workflow-authz.mock.ts index 59322e1c103..d6f6de6f287 100644 --- a/packages/testing/src/mocks/workflow-authz.mock.ts +++ b/packages/testing/src/mocks/workflow-authz.mock.ts @@ -3,7 +3,7 @@ import { vi } from 'vitest' /** * Real `WorkflowLockedError` subclass used by tests so `instanceof` checks in * route handlers behave the same as in production. Mirrors the shape exported - * by `@sim/workflow-authz`. + * by `@sim/platform-authz/workflow`. */ export class MockWorkflowLockedError extends Error { readonly status = 423 @@ -17,7 +17,7 @@ export class MockWorkflowLockedError extends Error { /** * Real `FolderLockedError` subclass used by tests so `instanceof` checks in * route handlers behave the same as in production. Mirrors the shape exported - * by `@sim/workflow-authz`. + * by `@sim/platform-authz/workflow`. */ export class MockFolderLockedError extends Error { readonly status = 423 @@ -31,7 +31,7 @@ export class MockFolderLockedError extends Error { /** * Real `FolderNotFoundError` subclass used by tests so `instanceof` checks in * route handlers behave the same as in production. Mirrors the shape exported - * by `@sim/workflow-authz`. + * by `@sim/platform-authz/workflow`. */ export class MockFolderNotFoundError extends Error { readonly status = 400 @@ -51,7 +51,7 @@ const unlockedStatus = { } /** - * Controllable mocks for the `@sim/workflow-authz` package. + * Controllable mocks for the `@sim/platform-authz/workflow` entry. * * Defaults assume permissive access (no lock, write allowed). Override with * `mockResolvedValue` per test when exercising the lock/permission paths. @@ -82,11 +82,11 @@ export const workflowAuthzMockFns = { } /** - * Static mock module for `@sim/workflow-authz`. + * Static mock module for `@sim/platform-authz/workflow`. * * @example * ```ts - * vi.mock('@sim/workflow-authz', () => workflowAuthzMock) + * vi.mock('@sim/platform-authz/workflow', () => workflowAuthzMock) * ``` */ export const workflowAuthzMock = { diff --git a/packages/testing/src/mocks/workflows-utils.mock.ts b/packages/testing/src/mocks/workflows-utils.mock.ts index 1c70412f014..89613a98907 100644 --- a/packages/testing/src/mocks/workflows-utils.mock.ts +++ b/packages/testing/src/mocks/workflows-utils.mock.ts @@ -40,7 +40,7 @@ export const workflowsUtilsMockFns = { * - `validateWorkflowPermissions` resolves to an authorized result * - Other functions resolve to sensible empty/success defaults * - * `authorizeWorkflowByWorkspacePermission` moved to `@sim/workflow-authz`; + * `authorizeWorkflowByWorkspacePermission` moved to `@sim/platform-authz/workflow`; * use `workflowAuthzMock` / `workflowAuthzMockFns` for that surface. * * @example From 13b5d215e951cb097116d7699dd4c5f091aa8720 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 19 Jun 2026 13:48:07 -0700 Subject: [PATCH 09/16] improvement(access-controls): docs, terminology, fix delete bug (#5141) * improvement(access-controls): dedup independent block names * improvement(access-controls): fix delete button, naming in perm group modal --- .../docs/en/workflows/blocks/webhook.mdx | 14 +++--- .../docs/en/workflows/triggers/sim.mdx | 14 +++--- apps/sim/blocks/blocks/sim_workspace_event.ts | 2 +- apps/sim/blocks/blocks/webhook_request.ts | 2 +- .../components/access-control.tsx | 46 +++++++++++-------- apps/sim/triggers/sim/workspace-event.test.ts | 6 +-- apps/sim/triggers/sim/workspace-event.ts | 2 +- 7 files changed, 46 insertions(+), 40 deletions(-) diff --git a/apps/docs/content/docs/en/workflows/blocks/webhook.mdx b/apps/docs/content/docs/en/workflows/blocks/webhook.mdx index 845347e340c..374fd03b913 100644 --- a/apps/docs/content/docs/en/workflows/blocks/webhook.mdx +++ b/apps/docs/content/docs/en/workflows/blocks/webhook.mdx @@ -1,6 +1,6 @@ --- -title: Webhook -description: The Webhook block sends an HTTP POST to an external endpoint, with automatic headers and optional signing. +title: Outgoing Webhook +description: The Outgoing Webhook block sends an HTTP POST to an external endpoint, with automatic headers and optional signing. pageType: reference --- @@ -8,7 +8,7 @@ import { Callout } from 'fumadocs-ui/components/callout' import { BlockPreview, WorkflowPreview, WEBHOOK_NOTIFY_WORKFLOW, WEBHOOK_TRIGGER_WORKFLOW } from '@/components/workflow-preview' import { FAQ } from '@/components/ui/faq' -The Webhook block sends HTTP POST requests to external webhook endpoints with automatic webhook headers and optional HMAC signing. +The Outgoing Webhook block sends HTTP POST requests to external webhook endpoints with automatic webhook headers and optional HMAC signing. @@ -77,16 +77,16 @@ Format the result, then POST it to a Slack, Discord, or custom endpoint. -When the Condition passes, the Webhook starts a process in another system. +When the Condition passes, the Outgoing Webhook starts a process in another system. -The Webhook block always uses POST. For other HTTP methods or more control, use the [API block](/workflows/blocks/api). +The Outgoing Webhook block always uses POST. For other HTTP methods or more control, use the [API block](/workflows/blocks/api). diff --git a/apps/docs/content/docs/en/workflows/triggers/sim.mdx b/apps/docs/content/docs/en/workflows/triggers/sim.mdx index 08edb72af8d..4f77b80e2fd 100644 --- a/apps/docs/content/docs/en/workflows/triggers/sim.mdx +++ b/apps/docs/content/docs/en/workflows/triggers/sim.mdx @@ -1,16 +1,16 @@ --- -title: Sim +title: Sim Workspace Events --- import { Callout } from 'fumadocs-ui/components/callout' import { Tab, Tabs } from 'fumadocs-ui/components/tabs' import { FAQ } from '@/components/ui/faq' -The Sim trigger runs a workflow when events happen in your workspace: another workflow's run fails or succeeds, a workflow is deployed, or an alert condition like a latency spike or cost threshold is met. Use it to build side-effect workflows — alerting, escalation, auto-remediation — composed from any blocks (Slack, email, webhooks, custom logic). +The Sim Workspace Events trigger runs a workflow when events happen in your workspace: another workflow's run fails or succeeds, a workflow is deployed, or an alert condition like a latency spike or cost threshold is met. Use it to build side-effect workflows — alerting, escalation, auto-remediation — composed from any blocks (Slack, email, webhooks, custom logic). ## Events -Pick one event per Sim trigger block: +Pick one event per Sim Workspace Events trigger block: **Plain events** — fire on every occurrence: @@ -68,8 +68,8 @@ All events include `event`, `timestamp`, `workflowId`, and `workflowName` (the s ## Behavior
    -
  • The workflow containing the Sim trigger must be deployed for events to fire.
  • -
  • Runs started by a Sim trigger never emit workspace events, so side-effect workflows cannot chain or loop.
  • +
  • The workflow containing the Sim Workspace Events trigger must be deployed for events to fire.
  • +
  • Runs started by a Sim Workspace Events trigger never emit workspace events, so side-effect workflows cannot chain or loop.
  • Alert conditions fire at most once per cooldown window (one hour, or the inactivity window for No Activity).
  • Event delivery is fire-and-forget: side-effect runs are billed like any other run and are subject to workspace rate limits.
@@ -80,8 +80,8 @@ All events include `event`, `timestamp`, `workflowId`, and `workflowName` (the s
diff --git a/apps/sim/blocks/blocks/sim_workspace_event.ts b/apps/sim/blocks/blocks/sim_workspace_event.ts index ea94088deeb..0a86aa777f5 100644 --- a/apps/sim/blocks/blocks/sim_workspace_event.ts +++ b/apps/sim/blocks/blocks/sim_workspace_event.ts @@ -8,7 +8,7 @@ export const SimWorkspaceEventBlock: BlockConfig = { // can scrape the type for icon-map keys; a test asserts it stays equal to // the constant. type: 'sim_workspace_event', - name: 'Sim', + name: 'Sim Workspace Events', description: 'Run this workflow when workspace events occur: run errors or successes, deployments, and alert conditions like latency or cost spikes.', category: 'triggers', diff --git a/apps/sim/blocks/blocks/webhook_request.ts b/apps/sim/blocks/blocks/webhook_request.ts index 6fad3395995..b34e78413d4 100644 --- a/apps/sim/blocks/blocks/webhook_request.ts +++ b/apps/sim/blocks/blocks/webhook_request.ts @@ -4,7 +4,7 @@ import type { RequestResponse } from '@/tools/http/types' export const WebhookRequestBlock: BlockConfig = { type: 'webhook_request', - name: 'Webhook', + name: 'Outgoing Webhook', description: 'Send a webhook request', longDescription: 'Send an HTTP POST request to a webhook URL with automatic webhook headers. Optionally sign the payload with HMAC-SHA256 for secure webhook delivery.', diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index da5b8b55e2d..0bf7de4dc47 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -1170,6 +1170,28 @@ export function AccessControl() { ) } + const deleteConfirmModal = ( + setDeletingGroup(null)} + srTitle='Delete Permission Group' + title='Delete Permission Group' + text={[ + 'Are you sure you want to delete ', + { text: deletingGroup?.name ?? 'this group', bold: true }, + '? ', + { text: 'All members will be removed from this group.', error: true }, + ' This action cannot be undone.', + ]} + confirm={{ + label: 'Delete', + onClick: confirmDelete, + pending: deletePermissionGroup.isPending, + pendingLabel: 'Deleting...', + }} + /> + ) + if (viewingGroup) { return ( <> @@ -1479,7 +1501,7 @@ export function AccessControl() { {filteredToolBlocks.length > 0 && (
- Tools + Integrations and Triggers
{filteredToolBlocks.map((block) => { @@ -1638,6 +1660,8 @@ export function AccessControl() { isAdding={bulkAddMembers.isPending} errorMessage={addMembersError} /> + + {deleteConfirmModal} ) } @@ -1784,25 +1808,7 @@ export function AccessControl() { /> - setDeletingGroup(null)} - srTitle='Delete Permission Group' - title='Delete Permission Group' - text={[ - 'Are you sure you want to delete ', - { text: deletingGroup?.name ?? 'this group', bold: true }, - '? ', - { text: 'All members will be removed from this group.', error: true }, - ' This action cannot be undone.', - ]} - confirm={{ - label: 'Delete', - onClick: confirmDelete, - pending: deletePermissionGroup.isPending, - pendingLabel: 'Deleting...', - }} - /> + {deleteConfirmModal} ) } diff --git a/apps/sim/triggers/sim/workspace-event.test.ts b/apps/sim/triggers/sim/workspace-event.test.ts index e338fb4cab5..9ddabb70d6f 100644 --- a/apps/sim/triggers/sim/workspace-event.test.ts +++ b/apps/sim/triggers/sim/workspace-event.test.ts @@ -34,9 +34,9 @@ describe('sim workspace event trigger registration', () => { }) }) - it('is named Sim', () => { - expect(SimWorkspaceEventBlock.name).toBe('Sim') - expect(simWorkspaceEventTrigger.name).toBe('Sim') + it('is named Sim Workspace Events', () => { + expect(SimWorkspaceEventBlock.name).toBe('Sim Workspace Events') + expect(simWorkspaceEventTrigger.name).toBe('Sim Workspace Events') }) }) diff --git a/apps/sim/triggers/sim/workspace-event.ts b/apps/sim/triggers/sim/workspace-event.ts index 5d455b709dc..27028aa2da6 100644 --- a/apps/sim/triggers/sim/workspace-event.ts +++ b/apps/sim/triggers/sim/workspace-event.ts @@ -10,7 +10,7 @@ import type { TriggerConfig } from '@/triggers/types' export const simWorkspaceEventTrigger: TriggerConfig = { id: SIM_WORKSPACE_EVENT_TRIGGER_ID, - name: 'Sim', + name: 'Sim Workspace Events', provider: SIM_TRIGGER_PROVIDER, description: 'Triggers when workspace events occur: run errors or successes, deployments, and alert conditions like latency or cost spikes', From 9d2a6ef0435294d012cb92063bab208591ab5ca8 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 19 Jun 2026 14:15:46 -0700 Subject: [PATCH 10/16] feat(logs): redact PII from workflow logs via configurable rules (#5136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(logs): redact PII from workflow logs via configurable rules Enterprise PII redaction for workflow execution logs, configured under Data Retention as org-scoped rules (each rule picks entity types + which workspaces it applies to). Reuses the guardrails Presidio engine in mask mode at the log-persist choke point, with a check-digit-validated VIN recognizer. Also adds per-workspace data-retention-hours overrides. * fix(logs): widen PII entity visibleValues to string[] for strict build typecheck * fix(logs): redact error/trigger/executionState; keep guardrails import lazy - Extend PII redaction to span error/errorMessage/toolCalls and top-level error/completionFailure/trigger/executionState (Bugbot: PII in execution metadata). executionState is safe to redact — resume reads from the separate pausedExecutions table, not the log copy. - Lazy-import validate_pii in pii-redaction so the Python/child_process guardrails module stays out of the static middleware/RSC graph. - Type the org retention mutation to the contract body (optional, non-null). * refactor(logs): drop per-workspace retention override; PII redaction stays org-scoped - Remove the unused per-workspace data-retention-hours override (no UI; superseded by workspace-scoped PII rules). Reverts cleanup-dispatcher to org-only retention, drops resolveEffectiveRetentionHours, the workspace.dataRetentionSettings column + migration, and the workspace data-retention route/contract/hooks. Fixes Bugbot's null-as-unset finding by removing the buggy path entirely; org retention behavior is unchanged. - Stop re-checking isWorkspaceOnEnterprisePlan at persist time (it returns false on transient errors, which would fail-open and leak PII). Enabled rules already imply entitlement; redact whenever rules apply (fail-safe). * fix(logs): redact oversized strings and executionData.environment - Drop the per-string size cap in PII redaction: oversized strings were left unmasked (leak). Nothing is skipped now; large payloads still fail-safe via the total-bytes ceiling + per-chunk timeout (scrub, never leak). - Add executionData.environment (incl. variables) to the redaction set. * refactor(logs): single-scope PII rules with most-specific-wins resolution Each rule now targets one scope — all workspaces (workspaceId: null) or a single workspace — with workspaceId unique across rules. Resolution is most-specific-wins (a workspace's own rule overrides the all rule), not union; an empty specific rule exempts that workspace. Matches Access Control's resolveWorkspaceGroup precedence. UI 'Applies to' becomes a single-select; Add rule disables when all scopes are taken. * feat(logs): default + workspace-overrides UI for PII redaction Reshape the PII redaction settings into a 'Default (all workspaces)' block plus a 'Workspace overrides' list, making the most-specific-wins precedence explicit (overrides replace the default; unlisted workspaces use it). Same data model (workspaceId null = default), UI only. * improvement(logs): clearer default/overrides PII UI Drop the uppercase section labels and the overrides description; gate the Workspace overrides section behind a configured default; use a single Delete action; 'Add redaction' creates the all-workspaces default and disappears once set. * fix(guardrails): handle stdin EPIPE in PII python spawns Attach an 'error' listener to the child's stdin in both runPythonScript (the batch masking hot path) and executePythonPIIDetection. A 256KB chunk can exceed the OS pipe buffer, so if the Python process exits mid-read (OOM/kill) the EPIPE emitted on stdin was unhandled and would crash the Node process. Funnel it into the promise rejection so the fail-safe scrub path handles it gracefully. * fix(logs): redact executionData.correlation The top-level correlation field is copied from pre-redaction trigger data, so webhook/schedule correlation values could persist unredacted. Add it to the redaction set alongside trigger/environment. * fix(logs): enforce unique PII rule scope server-side The contract accepted multiple rules with the same workspaceId (or several null all-rules); resolution is first-match, so duplicates could disagree with the UI. Add a schema refine rejecting duplicate scopes. * fix(logs): re-hydrate data-retention form on org switch The form hydrated once via a boolean ref, so switching the active org left stale retention days + PII rules and saves targeted the new org with old config. Key hydration on orgId so it re-loads per org. --- .../[id]/data-retention/route.ts | 24 +- .../components/data-retention-settings.tsx | 457 +++++++++++++++++- .../ee/data-retention/hooks/data-retention.ts | 3 +- apps/sim/lib/api/contracts/organization.ts | 14 +- apps/sim/lib/api/contracts/primitives.ts | 36 ++ apps/sim/lib/billing/retention.test.ts | 60 +++ apps/sim/lib/billing/retention.ts | 38 ++ apps/sim/lib/guardrails/pii-entities.ts | 128 +++++ apps/sim/lib/guardrails/validate_pii.py | 106 +++- apps/sim/lib/guardrails/validate_pii.ts | 190 +++++--- apps/sim/lib/logs/execution/logger.ts | 75 ++- .../lib/logs/execution/pii-redaction.test.ts | 120 +++++ apps/sim/lib/logs/execution/pii-redaction.ts | 181 +++++++ packages/db/schema.ts | 37 +- 14 files changed, 1364 insertions(+), 105 deletions(-) create mode 100644 apps/sim/lib/billing/retention.test.ts create mode 100644 apps/sim/lib/billing/retention.ts create mode 100644 apps/sim/lib/guardrails/pii-entities.ts create mode 100644 apps/sim/lib/logs/execution/pii-redaction.test.ts create mode 100644 apps/sim/lib/logs/execution/pii-redaction.ts diff --git a/apps/sim/app/api/organizations/[id]/data-retention/route.ts b/apps/sim/app/api/organizations/[id]/data-retention/route.ts index 65e291a00d3..e67d229df1a 100644 --- a/apps/sim/app/api/organizations/[id]/data-retention/route.ts +++ b/apps/sim/app/api/organizations/[id]/data-retention/route.ts @@ -1,37 +1,40 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' +import type { DataRetentionSettings } from '@sim/db/schema' import { member, organization } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { updateOrganizationDataRetentionContract } from '@/lib/api/contracts/organization' +import { + type OrganizationRetentionValues, + updateOrganizationDataRetentionContract, +} from '@/lib/api/contracts/organization' import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' -import { - CLEANUP_CONFIG, - type OrganizationRetentionSettings, -} from '@/lib/billing/cleanup-dispatcher' +import { CLEANUP_CONFIG } from '@/lib/billing/cleanup-dispatcher' import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription' import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('DataRetentionAPI') -function enterpriseDefaults(): OrganizationRetentionSettings { +function enterpriseDefaults(): OrganizationRetentionValues { return { logRetentionHours: CLEANUP_CONFIG['cleanup-logs'].defaults.enterprise, softDeleteRetentionHours: CLEANUP_CONFIG['cleanup-soft-deletes'].defaults.enterprise, taskCleanupHours: CLEANUP_CONFIG['cleanup-tasks'].defaults.enterprise, + piiRedaction: null, } } function normalizeConfigured( - settings: Partial | null | undefined -): OrganizationRetentionSettings { + settings: DataRetentionSettings | null | undefined +): OrganizationRetentionValues { return { logRetentionHours: settings?.logRetentionHours ?? null, softDeleteRetentionHours: settings?.softDeleteRetentionHours ?? null, taskCleanupHours: settings?.taskCleanupHours ?? null, + piiRedaction: settings?.piiRedaction?.rules ? { rules: settings.piiRedaction.rules } : null, } } @@ -152,7 +155,7 @@ export const PUT = withRouteHandler( } const current = normalizeConfigured(currentOrg.dataRetentionSettings) - const merged: OrganizationRetentionSettings = { ...current } + const merged: DataRetentionSettings = { ...current } if (body.logRetentionHours !== undefined) { merged.logRetentionHours = body.logRetentionHours } @@ -162,6 +165,9 @@ export const PUT = withRouteHandler( if (body.taskCleanupHours !== undefined) { merged.taskCleanupHours = body.taskCleanupHours } + if (body.piiRedaction !== undefined) { + merged.piiRedaction = body.piiRedaction + } const [updated] = await db .update(organization) diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index d0a306d6914..d1ef5aa1ad6 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -4,9 +4,24 @@ import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { toError } from '@sim/utils/errors' -import { Chip, ChipSelect, toast } from '@/components/emcn' +import { generateId } from '@sim/utils/id' +import { Plus } from 'lucide-react' +import { + Checkbox, + Chip, + ChipInput, + ChipModal, + ChipModalBody, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + ChipSelect, + Search, + toast, +} from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { isBillingEnabled } from '@/lib/core/config/env-flags' +import { PII_ENTITY_GROUPS, SUPPORTED_PII_ENTITIES } from '@/lib/guardrails/pii-entities' import { getUserRole } from '@/lib/workspaces/organization/utils' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { InfoNote } from '@/ee/components/info-note' @@ -16,9 +31,12 @@ import { useUpdateOrganizationRetention, } from '@/ee/data-retention/hooks/data-retention' import { useOrganizations } from '@/hooks/queries/organization' +import { useWorkspacesQuery } from '@/hooks/queries/workspace' const logger = createLogger('DataRetentionSettings') +const ENTITY_LABELS = SUPPORTED_PII_ENTITIES as Record + const DAY_OPTIONS = [ { value: '1', label: '1 day' }, { value: '3', label: '3 days' }, @@ -33,6 +51,16 @@ const DAY_OPTIONS = [ { value: 'never', label: 'Forever' }, ] as const +/** + * Local editable shape of a PII redaction rule. `workspaceId: null` is the + * all-workspaces default; a non-null id is a per-workspace override of it. + */ +interface RuleDraft { + id: string + entityTypes: string[] + workspaceId: string | null +} + function hoursToDisplayDays(hours: number | null): string { if (hours === null) return 'never' return String(Math.round(hours / 24)) @@ -43,6 +71,20 @@ function daysToHours(days: string): number | null { return Number(days) * 24 } +function normalizeRule(rule: RuleDraft): string { + return JSON.stringify({ + entityTypes: [...rule.entityTypes].sort(), + workspaceId: rule.workspaceId, + }) +} + +function entitySummary(entityTypes: string[]): string { + if (entityTypes.length === 0) return 'Not redacted' + const labels = entityTypes.map((t) => ENTITY_LABELS[t] ?? t) + if (labels.length <= 3) return labels.join(', ') + return `${labels.slice(0, 3).join(', ')} +${labels.length - 3} more` +} + interface RetentionSelectProps { value: string onChange: (value: string) => void @@ -60,6 +102,144 @@ function RetentionSelect({ value, onChange }: RetentionSelectProps) { return } +interface EntityCheckboxGridProps { + selected: string[] + onChange: (entityTypes: string[]) => void +} + +function EntityCheckboxGrid({ selected, onChange }: EntityCheckboxGridProps) { + const [search, setSearch] = useState('') + const query = search.trim().toLowerCase() + + const groups = PII_ENTITY_GROUPS.map((group) => ({ + label: group.label, + entities: query + ? group.entities.filter( + (e) => e.label.toLowerCase().includes(query) || e.value.toLowerCase().includes(query) + ) + : group.entities, + })).filter((group) => group.entities.length > 0) + + const visibleValues: string[] = groups.flatMap((g) => g.entities.map((e) => e.value)) + const allVisibleSelected = + visibleValues.length > 0 && visibleValues.every((v) => selected.includes(v)) + + function toggle(value: string) { + onChange(selected.includes(value) ? selected.filter((v) => v !== value) : [...selected, value]) + } + + function toggleAllVisible() { + if (allVisibleSelected) { + onChange(selected.filter((v) => !visibleValues.includes(v))) + } else { + onChange([...new Set([...selected, ...visibleValues])]) + } + } + + return ( +
+
+ setSearch(e.target.value)} + className='min-w-0 flex-1' + /> + + {allVisibleSelected ? 'Deselect all' : 'Select all'} + +
+
+ {groups.map((group) => ( +
+ {group.label} +
+ {group.entities.map((entity) => { + const checkboxId = `pii-${entity.value}` + return ( + + ) + })} +
+
+ ))} +
+
+ ) +} + +interface RuleModalProps { + draft: RuleDraft + isNew: boolean + isSaving: boolean + /** Workspaces selectable for an override (excludes those taken by other overrides). */ + workspaceOptions: { value: string; label: string }[] + onChange: (draft: RuleDraft) => void + onClose: () => void + onSave: () => void +} + +function RuleModal({ + draft, + isNew, + isSaving, + workspaceOptions, + onChange, + onClose, + onSave, +}: RuleModalProps) { + const isDefault = draft.workspaceId === null + return ( + + + {isDefault + ? 'Default redaction · all workspaces' + : isNew + ? 'Add workspace override' + : 'Edit workspace override'} + + + {!isDefault && ( + + onChange({ ...draft, workspaceId: value })} + options={workspaceOptions} + align='start' + /> + + )} + + onChange({ ...draft, entityTypes })} + /> + + + + + ) +} + export function DataRetentionSettings() { const { data: session, isPending: sessionPending } = useSession() const { data: orgsData, isLoading: orgsLoading } = useOrganizations() @@ -69,6 +249,12 @@ export function DataRetentionSettings() { const { data, isLoading: retentionLoading } = useOrganizationRetention(orgId) const updateMutation = useUpdateOrganizationRetention() + const { data: workspaces } = useWorkspacesQuery(Boolean(orgId)) + const workspaceOptions = (workspaces ?? []) + .filter((w) => w.organizationId === orgId) + .map((w) => ({ value: w.id, label: w.name })) + const workspaceName = (id: string) => + workspaceOptions.find((w) => w.value === id)?.label ?? 'Unknown workspace' const userEmail = session?.user?.email const userRole = getUserRole(activeOrganization, userEmail) @@ -77,31 +263,141 @@ export function DataRetentionSettings() { const [logDays, setLogDays] = useState('') const [softDeleteDays, setSoftDeleteDays] = useState('') const [taskCleanupDays, setTaskCleanupDays] = useState('') - const [savedLogDays, setSavedLogDays] = useState('') - const [savedSoftDeleteDays, setSavedSoftDeleteDays] = useState('') - const [savedTaskCleanupDays, setSavedTaskCleanupDays] = useState('') - const formInitializedRef = useRef(false) + const [savedHours, setSavedHours] = useState('') + const [rules, setRules] = useState([]) + const [modalDraft, setModalDraft] = useState(null) + const [modalOriginal, setModalOriginal] = useState(null) + const [modalIsNew, setModalIsNew] = useState(false) + const [showUnsaved, setShowUnsaved] = useState(false) + // Org the form was hydrated for; re-hydrate when the active org switches so + // saves don't target the new org with the previous org's config. + const hydratedOrgRef = useRef(null) + + function hoursSnapshot(log: string, soft: string, task: string): string { + return JSON.stringify({ log, soft, task }) + } useEffect(() => { - if (!data || formInitializedRef.current) return + if (!data || !orgId || hydratedOrgRef.current === orgId) return const log = hoursToDisplayDays(data.effective.logRetentionHours) const soft = hoursToDisplayDays(data.effective.softDeleteRetentionHours) const task = hoursToDisplayDays(data.effective.taskCleanupHours) setLogDays(log) setSoftDeleteDays(soft) setTaskCleanupDays(task) - setSavedLogDays(log) - setSavedSoftDeleteDays(soft) - setSavedTaskCleanupDays(task) - formInitializedRef.current = true - }, [data]) - - const hasChanges = - logDays !== savedLogDays || - softDeleteDays !== savedSoftDeleteDays || - taskCleanupDays !== savedTaskCleanupDays - - async function handleSave() { + setSavedHours(hoursSnapshot(log, soft, task)) + setRules( + (data.configured.piiRedaction?.rules ?? []).map((r) => ({ + id: r.id, + entityTypes: r.entityTypes, + workspaceId: r.workspaceId, + })) + ) + hydratedOrgRef.current = orgId + }, [data, orgId]) + + const hoursChanged = hoursSnapshot(logDays, softDeleteDays, taskCleanupDays) !== savedHours + const modalChanged = + modalDraft !== null && + modalOriginal !== null && + normalizeRule(modalDraft) !== normalizeRule(modalOriginal) + + const defaultRule = rules.find((r) => r.workspaceId === null) ?? null + const overrideRules = rules.filter((r) => r.workspaceId !== null) + const takenWorkspaceIds = new Set(overrideRules.map((r) => r.workspaceId as string)) + const freeWorkspaces = workspaceOptions.filter((w) => !takenWorkspaceIds.has(w.value)) + + /** Workspaces selectable for `draft` — excludes workspaces taken by OTHER overrides. */ + function overrideOptionsForDraft(draft: RuleDraft): { value: string; label: string }[] { + const otherTaken = new Set( + rules + .filter((r) => r.id !== draft.id && r.workspaceId !== null) + .map((r) => r.workspaceId as string) + ) + return workspaceOptions.filter((w) => !otherTaken.has(w.value)) + } + + async function persistRules(nextRules: RuleDraft[]) { + if (!orgId) return + await updateMutation.mutateAsync({ + orgId, + settings: { + piiRedaction: { + rules: nextRules.map((r) => ({ + id: r.id, + entityTypes: r.entityTypes, + workspaceId: r.workspaceId, + })), + }, + }, + }) + setRules(nextRules) + } + + function openEditDefault() { + const rule: RuleDraft = defaultRule ?? { id: generateId(), entityTypes: [], workspaceId: null } + setModalIsNew(defaultRule === null) + setModalOriginal(rule) + setModalDraft({ ...rule }) + } + + function openAddOverride() { + const workspaceId = freeWorkspaces[0]?.value + if (!workspaceId) return + const blank: RuleDraft = { id: generateId(), entityTypes: [], workspaceId } + setModalIsNew(true) + setModalOriginal(blank) + setModalDraft(blank) + } + + function openEditOverride(rule: RuleDraft) { + setModalIsNew(false) + setModalOriginal(rule) + setModalDraft({ ...rule }) + } + + function clearModal() { + setModalDraft(null) + setModalOriginal(null) + setShowUnsaved(false) + } + + function requestCloseModal() { + if (modalChanged) { + setShowUnsaved(true) + } else { + clearModal() + } + } + + async function saveModalRule() { + if (!modalDraft) return + const next = rules.some((r) => r.id === modalDraft.id) + ? rules.map((r) => (r.id === modalDraft.id ? modalDraft : r)) + : [...rules, modalDraft] + try { + await persistRules(next) + clearModal() + toast.success('PII redaction saved.') + } catch (error) { + const msg = toError(error).message + logger.error('Failed to save PII redaction', { error: msg }) + toast.error(msg) + } + } + + async function removeRule(id: string) { + try { + await persistRules(rules.filter((r) => r.id !== id)) + toast.success('PII redaction updated.') + } catch (error) { + const msg = toError(error).message + logger.error('Failed to update PII redaction', { error: msg }) + toast.error(msg) + } + } + + async function handleSaveHours() { if (!orgId) return try { await updateMutation.mutateAsync({ @@ -112,9 +408,7 @@ export function DataRetentionSettings() { taskCleanupHours: daysToHours(taskCleanupDays), }, }) - setSavedLogDays(logDays) - setSavedSoftDeleteDays(softDeleteDays) - setSavedTaskCleanupDays(taskCleanupDays) + setSavedHours(hoursSnapshot(logDays, softDeleteDays, taskCleanupDays)) toast.success('Data retention settings saved.') } catch (error) { const msg = toError(error).message @@ -166,8 +460,8 @@ export function DataRetentionSettings() {
{updateMutation.isPending ? 'Saving...' : 'Save'} @@ -198,8 +492,125 @@ export function DataRetentionSettings() {
+ +
+
+
+ + Default · all workspaces + + {!defaultRule && ( + + Add redaction + + )} +
+ {defaultRule && ( +
+ + {entitySummary(defaultRule.entityTypes)} + +
+ Edit + removeRule(defaultRule.id)} + disabled={updateMutation.isPending} + > + Delete + +
+
+ )} +
+ {defaultRule && ( +
+
+ + Workspace overrides + + + Add override + +
+ {overrideRules.length === 0 ? ( +

+ No overrides — every workspace uses the default. +

+ ) : ( +
+ {overrideRules.map((rule) => ( +
+
+ + {workspaceName(rule.workspaceId as string)} + + + {entitySummary(rule.entityTypes)} + +
+
+ openEditOverride(rule)}>Edit + removeRule(rule.id)} + disabled={updateMutation.isPending} + > + Delete + +
+
+ ))} + + Workspaces not listed use the default. + +
+ )} +
+ )} +
+
+ {modalDraft && ( + + )} + + setShowUnsaved(false)}>Unsaved changes + +

+ You have unsaved changes. Save them before closing? +

+
+ setShowUnsaved(false)} + cancelDisabled={updateMutation.isPending} + secondaryActions={[{ label: 'Discard', onClick: clearModal, variant: 'destructive' }]} + primaryAction={{ + label: updateMutation.isPending ? 'Saving...' : 'Save', + onClick: saveModalRule, + disabled: updateMutation.isPending, + }} + /> +
) } diff --git a/apps/sim/ee/data-retention/hooks/data-retention.ts b/apps/sim/ee/data-retention/hooks/data-retention.ts index 6a8e39cc066..f1e85a890a2 100644 --- a/apps/sim/ee/data-retention/hooks/data-retention.ts +++ b/apps/sim/ee/data-retention/hooks/data-retention.ts @@ -6,6 +6,7 @@ import { getOrganizationDataRetentionContract, type OrganizationDataRetention, type OrganizationRetentionValues, + type UpdateOrganizationDataRetentionBody, updateOrganizationDataRetentionContract, } from '@/lib/api/contracts/organization' @@ -39,7 +40,7 @@ export function useOrganizationRetention(orgId: string | undefined) { interface UpdateRetentionVariables { orgId: string - settings: Partial + settings: UpdateOrganizationDataRetentionBody } export function useUpdateOrganizationRetention() { diff --git a/apps/sim/lib/api/contracts/organization.ts b/apps/sim/lib/api/contracts/organization.ts index 1437174e189..8ba84cfc4e2 100644 --- a/apps/sim/lib/api/contracts/organization.ts +++ b/apps/sim/lib/api/contracts/organization.ts @@ -1,5 +1,9 @@ import { z } from 'zod' -import { workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { + type PiiRedactionSettings, + piiRedactionSettingsSchema, + workspaceIdSchema, +} from '@/lib/api/contracts/primitives' import { organizationBillingDataSchema } from '@/lib/api/contracts/subscription' import { defineRouteContract } from '@/lib/api/contracts/types' import { workspacePermissionSchema } from '@/lib/api/contracts/workspaces' @@ -98,16 +102,24 @@ const organizationDataRetentionHoursSchema = z .nullable() .optional() +export type { PiiRedactionSettings } + export const updateOrganizationDataRetentionBodySchema = z.object({ logRetentionHours: organizationDataRetentionHoursSchema, softDeleteRetentionHours: organizationDataRetentionHoursSchema, taskCleanupHours: organizationDataRetentionHoursSchema, + piiRedaction: piiRedactionSettingsSchema.optional(), }) +export type UpdateOrganizationDataRetentionBody = z.input< + typeof updateOrganizationDataRetentionBodySchema +> + const organizationRetentionValuesSchema = z.object({ logRetentionHours: z.number().int().nullable(), softDeleteRetentionHours: z.number().int().nullable(), taskCleanupHours: z.number().int().nullable(), + piiRedaction: piiRedactionSettingsSchema.nullable(), }) export type OrganizationRetentionValues = z.output diff --git a/apps/sim/lib/api/contracts/primitives.ts b/apps/sim/lib/api/contracts/primitives.ts index 9fac093c5af..e3e61484020 100644 --- a/apps/sim/lib/api/contracts/primitives.ts +++ b/apps/sim/lib/api/contracts/primitives.ts @@ -85,6 +85,42 @@ export const userFileSchema = z }) .passthrough() +/** A single PII redaction rule targeting one scope (all workspaces, or one). */ +export const piiRedactionRuleSchema = z.object({ + id: z.string().min(1), + name: z.string().max(100).optional(), + /** Presidio entity types to mask. Empty = redact nothing for this scope. */ + entityTypes: z.array(z.string().min(1, 'Entity type cannot be empty')).max(100), + /** null = all workspaces; otherwise the single targeted workspace. */ + workspaceId: z.string().min(1).nullable(), +}) + +export type PiiRedactionRule = z.output + +/** + * Enterprise PII redaction policy applied to workflow logs on persist. Each + * scope is unique: at most one all-workspaces rule (`workspaceId: null`) and at + * most one rule per workspace — resolution is most-specific-wins, so duplicate + * scopes would make masking depend on array order. + */ +export const piiRedactionSettingsSchema = z.object({ + rules: z + .array(piiRedactionRuleSchema) + .max(1000) + .refine( + (rules) => { + const scopes = rules.map((r) => r.workspaceId ?? '__all__') + return new Set(scopes).size === scopes.length + }, + { + message: + 'Each workspace (and the all-workspaces default) may have at most one PII redaction rule.', + } + ), +}) + +export type PiiRedactionSettings = z.output + export const booleanQueryFlagSchema = z.preprocess( (value) => { if (typeof value === 'boolean') return value diff --git a/apps/sim/lib/billing/retention.test.ts b/apps/sim/lib/billing/retention.test.ts new file mode 100644 index 00000000000..2852cb6a640 --- /dev/null +++ b/apps/sim/lib/billing/retention.test.ts @@ -0,0 +1,60 @@ +/** + * @vitest-environment node + */ +import type { DataRetentionSettings, PiiRedactionRule } from '@sim/db/schema' +import { describe, expect, it } from 'vitest' +import { resolveEffectivePiiRedaction } from '@/lib/billing/retention' + +function settings(rules: PiiRedactionRule[]): DataRetentionSettings { + return { piiRedaction: { rules } } +} + +describe('resolveEffectivePiiRedaction', () => { + const allRule: PiiRedactionRule = { + id: 'r-all', + entityTypes: ['EMAIL_ADDRESS', 'PHONE_NUMBER'], + workspaceId: null, + } + + it('applies the all-workspaces rule when the workspace has no specific rule', () => { + const result = resolveEffectivePiiRedaction({ + orgSettings: settings([allRule]), + workspaceId: 'ws-1', + }) + expect(result).toEqual({ enabled: true, entityTypes: ['EMAIL_ADDRESS', 'PHONE_NUMBER'] }) + }) + + it('lets a workspace-specific rule override the all rule', () => { + const result = resolveEffectivePiiRedaction({ + orgSettings: settings([allRule, { id: 'r-1', entityTypes: ['US_SSN'], workspaceId: 'ws-1' }]), + workspaceId: 'ws-1', + }) + expect(result).toEqual({ enabled: true, entityTypes: ['US_SSN'] }) + }) + + it('exempts a workspace when its specific rule has no entity types', () => { + const result = resolveEffectivePiiRedaction({ + orgSettings: settings([allRule, { id: 'r-1', entityTypes: [], workspaceId: 'ws-1' }]), + workspaceId: 'ws-1', + }) + expect(result).toEqual({ enabled: false, entityTypes: [] }) + }) + + it('is disabled when no rule matches and there is no all rule', () => { + const result = resolveEffectivePiiRedaction({ + orgSettings: settings([{ id: 'r-1', entityTypes: ['US_SSN'], workspaceId: 'ws-2' }]), + workspaceId: 'ws-1', + }) + expect(result).toEqual({ enabled: false, entityTypes: [] }) + }) + + it('is disabled when there are no rules', () => { + expect( + resolveEffectivePiiRedaction({ orgSettings: settings([]), workspaceId: 'ws-1' }) + ).toEqual({ enabled: false, entityTypes: [] }) + expect(resolveEffectivePiiRedaction({ orgSettings: null, workspaceId: 'ws-1' })).toEqual({ + enabled: false, + entityTypes: [], + }) + }) +}) diff --git a/apps/sim/lib/billing/retention.ts b/apps/sim/lib/billing/retention.ts new file mode 100644 index 00000000000..183dbb280e1 --- /dev/null +++ b/apps/sim/lib/billing/retention.ts @@ -0,0 +1,38 @@ +import type { DataRetentionSettings } from '@sim/db/schema' + +export interface EffectivePiiRedaction { + enabled: boolean + /** Presidio entity types to mask. Empty = redact all detected PII. */ + entityTypes: string[] +} + +export const DEFAULT_PII_REDACTION: EffectivePiiRedaction = { + enabled: false, + entityTypes: [], +} + +/** + * Resolve the effective PII redaction policy for a workspace from the org-level + * rules list, most-specific-wins (never unioned): the workspace's own rule takes + * precedence over the all-workspaces rule (`workspaceId: null`). A resolved rule + * with no entity types redacts nothing — so a workspace-specific empty rule + * exempts that workspace, overriding the all rule. Defensive about the + * loosely-typed JSON column. + */ +export function resolveEffectivePiiRedaction(params: { + orgSettings: DataRetentionSettings | null | undefined + workspaceId: string +}): EffectivePiiRedaction { + const rules = params.orgSettings?.piiRedaction?.rules + if (!Array.isArray(rules) || rules.length === 0) return DEFAULT_PII_REDACTION + + const rule = + rules.find((r) => r?.workspaceId === params.workspaceId) ?? + rules.find((r) => r?.workspaceId == null) + + const types = Array.isArray(rule?.entityTypes) + ? rule.entityTypes.filter((t): t is string => typeof t === 'string') + : [] + if (types.length === 0) return DEFAULT_PII_REDACTION + return { enabled: true, entityTypes: types } +} diff --git a/apps/sim/lib/guardrails/pii-entities.ts b/apps/sim/lib/guardrails/pii-entities.ts new file mode 100644 index 00000000000..0e67fe22ff7 --- /dev/null +++ b/apps/sim/lib/guardrails/pii-entities.ts @@ -0,0 +1,128 @@ +/** + * Client-safe catalog of Microsoft Presidio PII entity types. Single source of + * truth shared by the server-only validator (`validate_pii.ts`) and client + * settings UI — keep no node-only imports here. + */ +export const SUPPORTED_PII_ENTITIES = { + // Common/Global + CREDIT_CARD: 'Credit card number', + CRYPTO: 'Cryptocurrency wallet address', + DATE_TIME: 'Date or time', + EMAIL_ADDRESS: 'Email address', + IBAN_CODE: 'International Bank Account Number', + IP_ADDRESS: 'IP address', + NRP: 'Nationality, religious or political group', + LOCATION: 'Location', + PERSON: 'Person name', + PHONE_NUMBER: 'Phone number', + MEDICAL_LICENSE: 'Medical license number', + URL: 'URL', + VIN: 'Vehicle Identification Number', + + // USA + US_BANK_NUMBER: 'US bank account number', + US_DRIVER_LICENSE: 'US driver license', + US_ITIN: 'US Individual Taxpayer Identification Number', + US_PASSPORT: 'US passport number', + US_SSN: 'US Social Security Number', + + // UK + UK_NHS: 'UK NHS number', + UK_NINO: 'UK National Insurance Number', + + // Other countries + ES_NIF: 'Spanish NIF number', + ES_NIE: 'Spanish NIE number', + IT_FISCAL_CODE: 'Italian fiscal code', + IT_DRIVER_LICENSE: 'Italian driver license', + IT_VAT_CODE: 'Italian VAT code', + IT_PASSPORT: 'Italian passport', + IT_IDENTITY_CARD: 'Italian identity card', + PL_PESEL: 'Polish PESEL number', + SG_NRIC_FIN: 'Singapore NRIC/FIN', + SG_UEN: 'Singapore Unique Entity Number', + AU_ABN: 'Australian Business Number', + AU_ACN: 'Australian Company Number', + AU_TFN: 'Australian Tax File Number', + AU_MEDICARE: 'Australian Medicare number', + IN_PAN: 'Indian Permanent Account Number', + IN_AADHAAR: 'Indian Aadhaar number', + IN_VEHICLE_REGISTRATION: 'Indian vehicle registration', + IN_VOTER: 'Indian voter ID', + IN_PASSPORT: 'Indian passport', + FI_PERSONAL_IDENTITY_CODE: 'Finnish Personal Identity Code', + KR_RRN: 'Korean Resident Registration Number', + TH_TNIN: 'Thai National ID Number', +} as const + +export type PIIEntityType = keyof typeof SUPPORTED_PII_ENTITIES + +/** Flat `{ value, label }` options for entity-type pickers, in catalog order. */ +export const PII_ENTITY_OPTIONS: ReadonlyArray<{ value: PIIEntityType; label: string }> = + Object.entries(SUPPORTED_PII_ENTITIES).map(([value, label]) => ({ + value: value as PIIEntityType, + label, + })) + +/** Entity types grouped by region, for a grouped checkbox picker. */ +export const PII_ENTITY_GROUPS: ReadonlyArray<{ + label: string + entities: ReadonlyArray<{ value: PIIEntityType; label: string }> +}> = [ + { + label: 'Common', + entities: [ + 'PERSON', + 'EMAIL_ADDRESS', + 'PHONE_NUMBER', + 'CREDIT_CARD', + 'IP_ADDRESS', + 'LOCATION', + 'DATE_TIME', + 'URL', + 'IBAN_CODE', + 'CRYPTO', + 'NRP', + 'MEDICAL_LICENSE', + 'VIN', + ], + }, + { + label: 'United States', + entities: ['US_SSN', 'US_PASSPORT', 'US_DRIVER_LICENSE', 'US_BANK_NUMBER', 'US_ITIN'], + }, + { label: 'United Kingdom', entities: ['UK_NHS', 'UK_NINO'] }, + { + label: 'Other regions', + entities: [ + 'ES_NIF', + 'ES_NIE', + 'IT_FISCAL_CODE', + 'IT_DRIVER_LICENSE', + 'IT_VAT_CODE', + 'IT_PASSPORT', + 'IT_IDENTITY_CARD', + 'PL_PESEL', + 'SG_NRIC_FIN', + 'SG_UEN', + 'AU_ABN', + 'AU_ACN', + 'AU_TFN', + 'AU_MEDICARE', + 'IN_PAN', + 'IN_AADHAAR', + 'IN_VEHICLE_REGISTRATION', + 'IN_VOTER', + 'IN_PASSPORT', + 'FI_PERSONAL_IDENTITY_CODE', + 'KR_RRN', + 'TH_TNIN', + ], + }, +].map((group) => ({ + label: group.label, + entities: group.entities.map((value) => ({ + value: value as PIIEntityType, + label: SUPPORTED_PII_ENTITIES[value as PIIEntityType], + })), +})) diff --git a/apps/sim/lib/guardrails/validate_pii.py b/apps/sim/lib/guardrails/validate_pii.py index 570786b8d9e..d475b96e233 100644 --- a/apps/sim/lib/guardrails/validate_pii.py +++ b/apps/sim/lib/guardrails/validate_pii.py @@ -12,7 +12,7 @@ from typing import List, Dict, Any try: - from presidio_analyzer import AnalyzerEngine + from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer from presidio_anonymizer import AnonymizerEngine from presidio_anonymizer.entities import OperatorConfig except ImportError: @@ -24,6 +24,52 @@ sys.exit(0) +class VinRecognizer(PatternRecognizer): + """ + Recognizes Vehicle Identification Numbers (17 chars, A-Z/0-9 excluding + I/O/Q) and validates the ISO 3779 check digit (position 9). Validation makes + accidental matches on arbitrary 17-char codes (request ids, SKUs, tokens) + extremely unlikely. Note: some non-North-American VINs don't use the check + digit and will be skipped — an intentional bias toward precision. + """ + + _TRANSLIT = { + **{str(d): d for d in range(10)}, + "A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7, "H": 8, + "J": 1, "K": 2, "L": 3, "M": 4, "N": 5, "P": 7, "R": 9, + "S": 2, "T": 3, "U": 4, "V": 5, "W": 6, "X": 7, "Y": 8, "Z": 9, + } + _WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2] + + def validate_result(self, pattern_text: str): + vin = pattern_text.upper() + if len(vin) != 17: + return False + try: + total = sum(self._TRANSLIT[c] * w for c, w in zip(vin, self._WEIGHTS)) + except KeyError: + return False + check = total % 11 + expected = "X" if check == 10 else str(check) + return vin[8] == expected + + +def build_analyzer() -> "AnalyzerEngine": + """ + AnalyzerEngine with custom recognizers registered on top of the Presidio + defaults. Adds a check-digit-validated VIN recognizer. + """ + analyzer = AnalyzerEngine() + vin_pattern = Pattern(name="vin", regex=r"\b[A-HJ-NPR-Z0-9]{17}\b", score=0.7) + vin_recognizer = VinRecognizer( + supported_entity="VIN", + patterns=[vin_pattern], + context=["vin", "vehicle", "chassis"], + ) + analyzer.registry.add_recognizer(vin_recognizer) + return analyzer + + def detect_pii( text: str, entity_types: List[str], @@ -44,7 +90,7 @@ def detect_pii( """ try: # Initialize Presidio engines - analyzer = AnalyzerEngine() + analyzer = build_analyzer() # Analyze text for PII results = analyzer.analyze( @@ -124,18 +170,64 @@ def detect_pii( } +def mask_batch( + texts: List[str], + entity_types: List[str], + language: str = "en" +) -> Dict[str, Any]: + """ + Mask PII across many strings in a single process, reusing one analyzer + + anonymizer instance (engine construction loads the spaCy model and is the + dominant cost). Returns masked text per input, in input order; strings with + no detected PII are returned unchanged so callers can substitute directly. + """ + analyzer = build_analyzer() + anonymizer = AnonymizerEngine() + entities = entity_types if entity_types else None + + results = [] + for text in texts: + if not text: + results.append({"maskedText": text}) + continue + analyzer_results = analyzer.analyze(text=text, entities=entities, language=language) + if not analyzer_results: + results.append({"maskedText": text}) + continue + operators = { + entity_type: OperatorConfig("replace", {"new_value": f"<{entity_type}>"}) + for entity_type in set([r.entity_type for r in analyzer_results]) + } + anonymized = anonymizer.anonymize( + text=text, + analyzer_results=analyzer_results, + operators=operators + ) + results.append({"maskedText": anonymized.text}) + + return {"passed": True, "results": results} + + def main(): """Main entry point for CLI usage""" try: # Read input from stdin input_data = sys.stdin.read() data = json.loads(input_data) - - text = data.get("text", "") + entity_types = data.get("entityTypes", []) - mode = data.get("mode", "block") language = data.get("language", "en") - + + # Batch mask mode: an array of texts processed with one warm engine pair. + if "texts" in data: + texts = data.get("texts", []) + result = mask_batch(texts, entity_types, language) + print(f"__SIM_RESULT__={json.dumps(result)}") + return + + text = data.get("text", "") + mode = data.get("mode", "block") + # Validate inputs if not text: result = { @@ -145,7 +237,7 @@ def main(): } else: result = detect_pii(text, entity_types, mode, language) - + # Output result with marker for parsing print(f"__SIM_RESULT__={json.dumps(result)}") diff --git a/apps/sim/lib/guardrails/validate_pii.ts b/apps/sim/lib/guardrails/validate_pii.ts index 7f1ca2a89cd..ba6886bb92d 100644 --- a/apps/sim/lib/guardrails/validate_pii.ts +++ b/apps/sim/lib/guardrails/validate_pii.ts @@ -6,6 +6,13 @@ import { createLogger } from '@sim/logger' const logger = createLogger('PIIValidator') const DEFAULT_TIMEOUT = 30000 // 30 seconds +/** + * Max total bytes of text sent to a single Presidio subprocess. spaCy NER is the + * bottleneck, so large payloads are split into multiple short calls instead of + * one that risks the 30s timeout. + */ +const PII_CHUNK_MAX_BYTES = 256 * 1024 + export interface PIIValidationInput { text: string entityTypes: string[] // e.g., ["PERSON", "EMAIL_ADDRESS", "CREDIT_CARD"] @@ -70,6 +77,126 @@ export async function validatePII(input: PIIValidationInput): Promise { + if (texts.length === 0) return [] + + const chunks: string[][] = [] + let current: string[] = [] + let currentBytes = 0 + for (const text of texts) { + const bytes = Buffer.byteLength(text, 'utf8') + if (current.length > 0 && currentBytes + bytes > PII_CHUNK_MAX_BYTES) { + chunks.push(current) + current = [] + currentBytes = 0 + } + current.push(text) + currentBytes += bytes + } + if (current.length > 0) chunks.push(current) + + const masked: string[] = [] + for (const chunk of chunks) { + const result = await runPythonScript({ + texts: chunk, + entityTypes, + mode: 'mask', + language, + }) + if (!result.passed || !result.results || result.results.length !== chunk.length) { + throw new Error(result.error || 'PII batch masking returned an unexpected result') + } + for (const item of result.results) masked.push(item.maskedText) + } + + return masked +} + +/** + * Spawn the Presidio Python script, write the payload to stdin as JSON, and parse + * the `__SIM_RESULT__=` marker from stdout. Rejects on non-zero exit, timeout, + * spawn failure, or a missing/unparseable marker. + */ +function runPythonScript(payload: Record): Promise { + return new Promise((resolve, reject) => { + const guardrailsDir = path.join(process.cwd(), 'lib/guardrails') + const scriptPath = path.join(guardrailsDir, 'validate_pii.py') + const venvPython = path.join(guardrailsDir, 'venv/bin/python3') + const pythonCmd = fs.existsSync(venvPython) ? venvPython : 'python3' + + const python = spawn(pythonCmd, [scriptPath]) + let stdout = '' + let stderr = '' + + const timeout = setTimeout(() => { + python.kill() + reject(new Error('PII processing timeout')) + }, DEFAULT_TIMEOUT) + + // stdin errors (e.g. EPIPE when the child exits before draining the payload — + // chunks can exceed the OS pipe buffer) emit on stdin, not the process. Without + // a listener Node throws an unhandled 'error' and crashes; funnel it into the + // promise so the caller's fail-safe scrub path handles it. + python.stdin.on('error', (error: Error) => { + clearTimeout(timeout) + reject(new Error(`PII script stdin error: ${error.message}`)) + }) + python.stdin.write(JSON.stringify(payload)) + python.stdin.end() + python.stdout.on('data', (data) => { + stdout += data.toString() + }) + python.stderr.on('data', (data) => { + stderr += data.toString() + }) + + python.on('close', (code) => { + clearTimeout(timeout) + if (code !== 0) { + reject(new Error(stderr || `PII script exited with code ${code}`)) + return + } + const prefix = '__SIM_RESULT__=' + const marker = stdout.split('\n').find((l) => l.startsWith(prefix)) + if (!marker) { + reject(new Error(`No result marker in PII script output: ${stdout.substring(0, 200)}`)) + return + } + try { + resolve(JSON.parse(marker.slice(prefix.length)) as T) + } catch (error: any) { + reject(new Error(`Failed to parse PII script result: ${error.message}`)) + } + }) + + python.on('error', (error) => { + clearTimeout(timeout) + reject( + new Error( + `Failed to execute Python: ${error.message}. Make sure Python 3 and Presidio are installed.` + ) + ) + }) + }) +} + /** * Execute Python PII detection script */ @@ -107,6 +234,12 @@ async function executePythonPIIDetection( mode, language, }) + // See runPythonScript: stdin errors (EPIPE on early child exit) must be + // caught here or Node throws an unhandled 'error' and crashes the process. + python.stdin.on('error', (error: Error) => { + clearTimeout(timeout) + reject(new Error(`Failed to write to Python: ${error.message}`)) + }) python.stdin.write(inputData) python.stdin.end() @@ -184,59 +317,4 @@ async function executePythonPIIDetection( }) } -/** - * List of all supported PII entity types - * Based on Microsoft Presidio's supported entities - */ -export const SUPPORTED_PII_ENTITIES = { - // Common/Global - CREDIT_CARD: 'Credit card number', - CRYPTO: 'Cryptocurrency wallet address', - DATE_TIME: 'Date or time', - EMAIL_ADDRESS: 'Email address', - IBAN_CODE: 'International Bank Account Number', - IP_ADDRESS: 'IP address', - NRP: 'Nationality, religious or political group', - LOCATION: 'Location', - PERSON: 'Person name', - PHONE_NUMBER: 'Phone number', - MEDICAL_LICENSE: 'Medical license number', - URL: 'URL', - - // USA - US_BANK_NUMBER: 'US bank account number', - US_DRIVER_LICENSE: 'US driver license', - US_ITIN: 'US Individual Taxpayer Identification Number', - US_PASSPORT: 'US passport number', - US_SSN: 'US Social Security Number', - - // UK - UK_NHS: 'UK NHS number', - UK_NINO: 'UK National Insurance Number', - - // Other countries - ES_NIF: 'Spanish NIF number', - ES_NIE: 'Spanish NIE number', - IT_FISCAL_CODE: 'Italian fiscal code', - IT_DRIVER_LICENSE: 'Italian driver license', - IT_VAT_CODE: 'Italian VAT code', - IT_PASSPORT: 'Italian passport', - IT_IDENTITY_CARD: 'Italian identity card', - PL_PESEL: 'Polish PESEL number', - SG_NRIC_FIN: 'Singapore NRIC/FIN', - SG_UEN: 'Singapore Unique Entity Number', - AU_ABN: 'Australian Business Number', - AU_ACN: 'Australian Company Number', - AU_TFN: 'Australian Tax File Number', - AU_MEDICARE: 'Australian Medicare number', - IN_PAN: 'Indian Permanent Account Number', - IN_AADHAAR: 'Indian Aadhaar number', - IN_VEHICLE_REGISTRATION: 'Indian vehicle registration', - IN_VOTER: 'Indian voter ID', - IN_PASSPORT: 'Indian passport', - FI_PERSONAL_IDENTITY_CODE: 'Finnish Personal Identity Code', - KR_RRN: 'Korean Resident Registration Number', - TH_TNIN: 'Thai National ID Number', -} as const - -export type PIIEntityType = keyof typeof SUPPORTED_PII_ENTITIES +export { type PIIEntityType, SUPPORTED_PII_ENTITIES } from '@/lib/guardrails/pii-entities' diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index e8c6edd7c55..9a77a0ec427 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -1,11 +1,13 @@ import { db } from '@sim/db' import { member, + organization, usageLog, userStats, user as userTable, workflow, workflowExecutionLogs, + workspace, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' @@ -24,6 +26,7 @@ import { recordUsage, stableEventKey, } from '@/lib/billing/core/usage-log' +import { resolveEffectivePiiRedaction } from '@/lib/billing/retention' import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import { isBillingEnabled } from '@/lib/core/config/env-flags' import { redactApiKeys } from '@/lib/core/security/redaction' @@ -32,6 +35,7 @@ import { collectLargeValueReferenceKeys, replaceLargeValueReferenceKeysWithClient, } from '@/lib/execution/payloads/large-value-metadata' +import { type RedactablePayload, redactPIIFromExecution } from '@/lib/logs/execution/pii-redaction' import { snapshotService } from '@/lib/logs/execution/snapshot/service' import { externalizeExecutionData, @@ -585,6 +589,37 @@ export class ExecutionLogger implements IExecutionLoggerService { } } + /** + * Mask PII from log content before persistence when the execution's workspace + * (via workspace override or org default) has enterprise PII redaction enabled. + * Resolved at persist time so both the inline and externalized write paths are + * covered. Returns the payload unchanged when disabled or non-enterprise. + */ + private async applyPiiRedaction( + workspaceId: string | null, + payload: RedactablePayload + ): Promise { + if (!workspaceId) return payload + + const [row] = await db + .select({ orgSettings: organization.dataRetentionSettings }) + .from(workspace) + .leftJoin(organization, eq(organization.id, workspace.organizationId)) + .where(eq(workspace.id, workspaceId)) + .limit(1) + if (!row) return payload + + // Rules are only writable by enterprise orgs (route-gated), so an enabled + // rule already implies entitlement. We deliberately do NOT re-check + // `isWorkspaceOnEnterprisePlan` here: it returns false on transient lookup + // errors, which would silently skip masking and leak PII (fail-open). When + // rules are present we always redact (fail-safe; over-redaction at worst). + const config = resolveEffectivePiiRedaction({ orgSettings: row.orgSettings, workspaceId }) + if (!config.enabled) return payload + + return redactPIIFromExecution(payload, { entityTypes: config.entityTypes }) + } + async completeWorkflowExecution(params: { executionId: string endedAt: string @@ -720,6 +755,26 @@ export class ExecutionLogger implements IExecutionLoggerService { const redactedWorkflowInput = filteredWorkflowInput !== undefined ? redactApiKeys(filteredWorkflowInput) : undefined + const pii = await this.applyPiiRedaction(existingLog?.workspaceId ?? null, { + traceSpans: redactedTraceSpans, + finalOutput: redactedFinalOutput, + ...(redactedWorkflowInput !== undefined ? { workflowInput: redactedWorkflowInput } : {}), + ...(builtExecutionData.error !== undefined ? { error: builtExecutionData.error } : {}), + ...(builtExecutionData.completionFailure !== undefined + ? { completionFailure: builtExecutionData.completionFailure } + : {}), + ...(builtExecutionData.trigger !== undefined ? { trigger: builtExecutionData.trigger } : {}), + ...(builtExecutionData.executionState !== undefined + ? { executionState: builtExecutionData.executionState } + : {}), + ...(builtExecutionData.environment !== undefined + ? { environment: builtExecutionData.environment } + : {}), + ...(builtExecutionData.correlation !== undefined + ? { correlation: builtExecutionData.correlation } + : {}), + }) + const rawDurationMs = isResume && existingLog?.startedAt ? new Date(endedAt).getTime() - new Date(existingLog.startedAt).getTime() @@ -731,9 +786,23 @@ export class ExecutionLogger implements IExecutionLoggerService { const cleanExecutionData: ExecutionData = { ...builtExecutionData, - traceSpans: redactedTraceSpans, - finalOutput: redactedFinalOutput, - ...(redactedWorkflowInput !== undefined ? { workflowInput: redactedWorkflowInput } : {}), + traceSpans: pii.traceSpans as TraceSpan[], + finalOutput: pii.finalOutput as BlockOutputData, + ...(pii.workflowInput !== undefined ? { workflowInput: pii.workflowInput } : {}), + ...(pii.error !== undefined ? { error: pii.error as string } : {}), + ...(pii.completionFailure !== undefined + ? { completionFailure: pii.completionFailure as string } + : {}), + ...(pii.trigger !== undefined ? { trigger: pii.trigger as ExecutionTrigger } : {}), + ...(pii.executionState !== undefined + ? { executionState: pii.executionState as SerializableExecutionState } + : {}), + ...(pii.environment !== undefined + ? { environment: pii.environment as ExecutionEnvironment } + : {}), + ...(pii.correlation !== undefined + ? { correlation: pii.correlation as ExecutionData['correlation'] } + : {}), } stripSpanCosts((cleanExecutionData as Record).traceSpans) diff --git a/apps/sim/lib/logs/execution/pii-redaction.test.ts b/apps/sim/lib/logs/execution/pii-redaction.test.ts new file mode 100644 index 00000000000..dccbc59cc38 --- /dev/null +++ b/apps/sim/lib/logs/execution/pii-redaction.test.ts @@ -0,0 +1,120 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockMaskPIIBatch } = vi.hoisted(() => ({ + mockMaskPIIBatch: vi.fn(), +})) + +vi.mock('@/lib/guardrails/validate_pii', () => ({ + maskPIIBatch: mockMaskPIIBatch, +})) + +import { REDACTION_FAILED_MARKER, redactPIIFromExecution } from '@/lib/logs/execution/pii-redaction' + +describe('redactPIIFromExecution', () => { + beforeEach(() => { + vi.clearAllMocks() + // Default: echo each input uppercased so we can assert substitution by position. + mockMaskPIIBatch.mockImplementation(async (texts: string[]) => texts.map((t) => `MASKED(${t})`)) + }) + + it('collects and masks string leaves recursively, preserving structure', async () => { + const payload = { + traceSpans: [ + { + blockId: 'b1', + status: 'success', + input: { email: 'a@b.com' }, + output: { text: 'hello' }, + children: [{ blockId: 'c1', output: { nested: 'deep' } }], + }, + ], + finalOutput: { answer: 'world' }, + workflowInput: 'start', + } + + const result = await redactPIIFromExecution(payload, { entityTypes: ['EMAIL_ADDRESS'] }) + + const span = (result.traceSpans as any[])[0] + expect(span.blockId).toBe('b1') + expect(span.status).toBe('success') + expect(span.input.email).toBe('MASKED(a@b.com)') + expect(span.output.text).toBe('MASKED(hello)') + expect(span.children[0].output.nested).toBe('MASKED(deep)') + expect((result.finalOutput as any).answer).toBe('MASKED(world)') + expect(result.workflowInput).toBe('MASKED(start)') + expect(mockMaskPIIBatch).toHaveBeenCalledTimes(1) + expect(mockMaskPIIBatch.mock.calls[0][0]).toEqual([ + 'a@b.com', + 'hello', + 'deep', + 'world', + 'start', + ]) + }) + + it('does not mutate the original payload', async () => { + const payload = { finalOutput: { answer: 'world' } } + await redactPIIFromExecution(payload, { entityTypes: [] }) + expect(payload.finalOutput.answer).toBe('world') + }) + + it('scrubs all eligible strings when masking throws (no leak)', async () => { + mockMaskPIIBatch.mockRejectedValueOnce(new Error('presidio down')) + const payload = { + traceSpans: [{ output: { text: 'secret@x.com' } }], + finalOutput: 'another secret', + } + + const result = await redactPIIFromExecution(payload, { entityTypes: [] }) + + expect((result.traceSpans as any[])[0].output.text).toBe(REDACTION_FAILED_MARKER) + expect(result.finalOutput).toBe(REDACTION_FAILED_MARKER) + }) + + it('masks large strings too (never left unredacted)', async () => { + const big = 'x'.repeat(200 * 1024) + const payload = { finalOutput: { big, small: 'pii' } } + + const result = await redactPIIFromExecution(payload, { entityTypes: [] }) + + expect((result.finalOutput as any).big).toBe(`MASKED(${big})`) + expect((result.finalOutput as any).small).toBe('MASKED(pii)') + expect(mockMaskPIIBatch.mock.calls[0][0]).toEqual([big, 'pii']) + }) + + it('masks span error/errorMessage and top-level error, trigger, executionState, environment', async () => { + const payload = { + traceSpans: [{ blockId: 'b1', error: 'failed for bob@x.com', errorMessage: 'bad input z' }], + error: 'run failed: a@b.com', + completionFailure: 'cancelled by c@d.com', + trigger: { type: 'webhook', data: { from: 'caller@x.com' } }, + executionState: { status: 'completed', note: 'state for e@f.com' }, + environment: { variables: { CONTACT: 'admin@x.com' } }, + correlation: { source: 'corr@x.com' }, + } + + const result = await redactPIIFromExecution(payload, { entityTypes: ['EMAIL_ADDRESS'] }) + + const span = (result.traceSpans as any[])[0] + expect(span.blockId).toBe('b1') + expect(span.error).toBe('MASKED(failed for bob@x.com)') + expect(span.errorMessage).toBe('MASKED(bad input z)') + expect(result.error).toBe('MASKED(run failed: a@b.com)') + expect(result.completionFailure).toBe('MASKED(cancelled by c@d.com)') + expect((result.trigger as any).type).toBe('MASKED(webhook)') + expect((result.trigger as any).data.from).toBe('MASKED(caller@x.com)') + expect((result.executionState as any).note).toBe('MASKED(state for e@f.com)') + expect((result.environment as any).variables.CONTACT).toBe('MASKED(admin@x.com)') + expect((result.correlation as any).source).toBe('MASKED(corr@x.com)') + }) + + it('returns payload unchanged when there is nothing to mask', async () => { + const payload = { traceSpans: [{ blockId: 'b1', count: 5 }] } + const result = await redactPIIFromExecution(payload, { entityTypes: [] }) + expect(result).toBe(payload) + expect(mockMaskPIIBatch).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/logs/execution/pii-redaction.ts b/apps/sim/lib/logs/execution/pii-redaction.ts new file mode 100644 index 00000000000..7b4794fd483 --- /dev/null +++ b/apps/sim/lib/logs/execution/pii-redaction.ts @@ -0,0 +1,181 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' + +const logger = createLogger('PiiRedaction') + +/** Replaces text we could not safely mask, so PII is never persisted on failure. */ +export const REDACTION_FAILED_MARKER = '[REDACTION_FAILED]' + +/** + * Upper bound on total text masked for one execution. Beyond this we scrub the + * whole payload rather than spend minutes in NER (never leave it unmasked). + * Typical inline logs (≤3MB) stay well under. Individual strings are never + * skipped by size — they would otherwise persist unredacted. + */ +const PII_MAX_TOTAL_BYTES = 16 * 1024 * 1024 + +export interface PiiRedactionOptions { + /** Presidio entity types to mask. Empty = redact all detected PII. */ + entityTypes: string[] + language?: string +} + +export interface RedactablePayload { + traceSpans?: unknown + finalOutput?: unknown + workflowInput?: unknown + error?: unknown + completionFailure?: unknown + trigger?: unknown + executionState?: unknown + environment?: unknown + correlation?: unknown +} + +/** Keys of {@link RedactablePayload} processed by the redactor, in order. */ +const REDACTABLE_KEYS: (keyof RedactablePayload)[] = [ + 'traceSpans', + 'finalOutput', + 'workflowInput', + 'error', + 'completionFailure', + 'trigger', + 'executionState', + 'environment', + 'correlation', +] + +/** Trace-span fields that carry runtime content (and therefore possible PII). */ +const SPAN_CONTENT_FIELDS = [ + 'input', + 'output', + 'thinking', + 'modelToolCalls', + 'toolCalls', + 'error', + 'errorMessage', +] as const + +function isEligibleString(value: string): boolean { + return value.length > 0 +} + +/** + * Rebuild `value` replacing every eligible string leaf with `handle(leaf)`. + * Used for both collection (handle records and returns the input) and + * substitution (handle returns the masked value), so traversal order and + * eligibility are guaranteed identical across the two passes. + */ +function transformStrings(value: unknown, handle: (s: string) => string): unknown { + if (typeof value === 'string') { + return isEligibleString(value) ? handle(value) : value + } + if (Array.isArray(value)) { + return value.map((item) => transformStrings(item, handle)) + } + if (value !== null && typeof value === 'object') { + const out: Record = {} + for (const [key, v] of Object.entries(value)) { + out[key] = transformStrings(v, handle) + } + return out + } + return value +} + +/** + * Redact a trace span: only its content fields ({@link SPAN_CONTENT_FIELDS}) and + * nested `children` are walked, leaving structural metadata (blockId, name, + * status, timing) untouched so log correlation/display is preserved. + */ +function transformSpan(span: unknown, handle: (s: string) => string): unknown { + if (span === null || typeof span !== 'object' || Array.isArray(span)) { + return transformStrings(span, handle) + } + const source = span as Record + const out: Record = { ...source } + for (const field of SPAN_CONTENT_FIELDS) { + if (field in out) out[field] = transformStrings(out[field], handle) + } + if (Array.isArray(source.children)) { + out.children = source.children.map((child) => transformSpan(child, handle)) + } + return out +} + +function transformUnit( + key: keyof RedactablePayload, + value: unknown, + handle: (s: string) => string +): unknown { + if (key === 'traceSpans' && Array.isArray(value)) { + return value.map((span) => transformSpan(span, handle)) + } + return transformStrings(value, handle) +} + +/** + * Mask PII across an execution's `traceSpans` / `finalOutput` / `workflowInput`. + * + * All eligible string leaves are collected in one deterministic pass and masked + * in a single batched (byte-chunked) Presidio call — so subprocess count scales + * with payload size, not block count. Each unit is then rebuilt independently + * from the masked slice, preserving the JSON structure (Presidio never sees the + * envelope). On a hard masking failure or when the payload exceeds the ceiling, + * eligible strings are replaced with {@link REDACTION_FAILED_MARKER} rather than + * left unredacted — PII is never persisted on the failure path. + */ +export async function redactPIIFromExecution( + payload: RedactablePayload, + options: PiiRedactionOptions +): Promise { + const { entityTypes } = options + const language = options.language ?? 'en' + + const units = REDACTABLE_KEYS.filter((key) => payload[key] !== undefined).map((key) => ({ + key, + value: payload[key], + })) + + const collected: string[] = [] + let totalBytes = 0 + for (const unit of units) { + transformUnit(unit.key, unit.value, (s) => { + collected.push(s) + totalBytes += Buffer.byteLength(s, 'utf8') + return s + }) + } + + if (collected.length === 0) return payload + + let masked: string[] + if (totalBytes > PII_MAX_TOTAL_BYTES) { + logger.warn('Execution exceeds PII redaction ceiling; scrubbing text', { + totalBytes, + ceiling: PII_MAX_TOTAL_BYTES, + }) + masked = collected.map(() => REDACTION_FAILED_MARKER) + } else { + try { + // Lazy import keeps the Python-spawning guardrails module (child_process + + // a `lib/guardrails` dir reference) out of the static middleware/RSC graph; + // it's only loaded at runtime on the Node log-persist path. + const { maskPIIBatch } = await import('@/lib/guardrails/validate_pii') + masked = await maskPIIBatch(collected, entityTypes, language) + } catch (error) { + logger.error('PII masking failed; scrubbing text to avoid leaking PII', { + error: getErrorMessage(error), + stringCount: collected.length, + }) + masked = collected.map(() => REDACTION_FAILED_MARKER) + } + } + + let index = 0 + const result: RedactablePayload = { ...payload } + for (const unit of units) { + result[unit.key] = transformUnit(unit.key, unit.value, () => masked[index++]) + } + return result +} diff --git a/packages/db/schema.ts b/packages/db/schema.ts index f82fb8b2397..dd93e988d3d 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1063,6 +1063,37 @@ export const chat = pgTable( } ) +/** + * A single PII redaction rule. Lives in the org-level + * {@link DataRetentionSettings.piiRedaction} rules list. Each rule targets one + * scope — all workspaces (`workspaceId: null`) or a single workspace — and + * `workspaceId` is unique across rules. Resolution is most-specific-wins: a + * workspace's own rule overrides the all-workspaces rule (never unioned). + */ +export interface PiiRedactionRule { + id: string + name?: string + /** Presidio entity types to mask. Empty = redact nothing for this scope. */ + entityTypes: string[] + /** `null` = all workspaces; otherwise the single targeted workspace. */ + workspaceId: string | null +} + +/** + * Org-level data retention + governance settings. Retention-hours fall back to + * plan defaults when unset. `piiRedaction.rules` are org-scoped; each rule + * selects which workspaces it applies to. + */ +export interface DataRetentionSettings { + logRetentionHours?: number | null + softDeleteRetentionHours?: number | null + taskCleanupHours?: number | null + /** Enterprise PII redaction rules applied to workflow logs on persist. */ + piiRedaction?: { + rules?: PiiRedactionRule[] + } | null +} + export const organization = pgTable('organization', { id: text('id').primaryKey(), name: text('name').notNull(), @@ -1082,11 +1113,7 @@ export const organization = pgTable('organization', { privacyUrl?: string hidePoweredBySim?: boolean }>(), - dataRetentionSettings: json('data_retention_settings').$type<{ - logRetentionHours?: number | null - softDeleteRetentionHours?: number | null - taskCleanupHours?: number | null - }>(), + dataRetentionSettings: json('data_retention_settings').$type(), orgUsageLimit: decimal('org_usage_limit'), /** * Storage upload/delete hot-path tracker for org-scoped plans. From 208d135daccf626a3fc1832eb813925885d34c03 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 19 Jun 2026 14:19:13 -0700 Subject: [PATCH 11/16] feat(enrichment): add enrichment details sidebar with cost + provider cascade (#5139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(enrichment): add enrichment details sidebar with cost + provider cascade * fix(enrichment): address review — persist detail on cancel/skip, exclude not_run from ran count, refetch on panel open * fix(enrichment): keep cascade detail sticky on upsert; mark unattempted providers not_run on abort * fix(enrichment): show Cancelled in details panel for aborted runs --- .../enrichment/[groupId]/route.test.ts | 118 + .../[rowId]/enrichment/[groupId]/route.ts | 51 + .../components/trace-view/trace-view.tsx | 28 +- .../logs/components/log-details/utils.ts | 25 + .../enrichment-details/enrichment-details.tsx | 389 + .../components/enrichment-details/index.ts | 1 + .../tables/[tableId]/components/index.ts | 1 + .../components/table-grid/table-grid.tsx | 38 +- .../[workspaceId]/tables/[tableId]/table.tsx | 30 +- .../background/workflow-column-execution.ts | 17 +- apps/sim/enrichments/run.test.ts | 155 + apps/sim/enrichments/run.ts | 136 +- apps/sim/hooks/queries/tables.ts | 44 + apps/sim/lib/api/contracts/tables.ts | 24 + apps/sim/lib/table/rows/executions.ts | 52 +- apps/sim/lib/table/types.ts | 60 + bun.lock | 1 + ...able_row_executions_enrichment_details.sql | 1 + .../db/migrations/meta/0244_snapshot.json | 16735 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 7 + scripts/check-api-validation-contracts.ts | 4 +- 22 files changed, 17879 insertions(+), 45 deletions(-) create mode 100644 apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts create mode 100644 apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/index.ts create mode 100644 apps/sim/enrichments/run.test.ts create mode 100644 packages/db/migrations/0244_table_row_executions_enrichment_details.sql create mode 100644 packages/db/migrations/meta/0244_snapshot.json diff --git a/apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts b/apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts new file mode 100644 index 00000000000..8ef809a71a1 --- /dev/null +++ b/apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts @@ -0,0 +1,118 @@ +/** + * @vitest-environment node + */ +import { hybridAuthMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { EnrichmentRunDetail, TableDefinition } from '@/lib/table' + +const { mockCheckAccess, mockLoadEnrichmentDetail } = vi.hoisted(() => ({ + mockCheckAccess: vi.fn(), + mockLoadEnrichmentDetail: vi.fn(), +})) + +vi.mock('@/lib/table/rows/executions', () => ({ + loadEnrichmentDetail: mockLoadEnrichmentDetail, +})) +vi.mock('@/app/api/table/utils', async () => { + const { NextResponse } = await import('next/server') + return { + checkAccess: mockCheckAccess, + accessError: (result: { status: number }) => + NextResponse.json({ error: 'denied' }, { status: result.status }), + } +}) + +import { GET } from '@/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route' + +function buildTable(): TableDefinition { + return { + id: 'tbl_1', + name: 'People', + description: null, + schema: { columns: [] }, + metadata: null, + rowCount: 1, + maxRows: 1_000_000, + workspaceId: 'workspace-1', + createdBy: 'user-1', + archivedAt: null, + createdAt: new Date(), + updatedAt: new Date(), + } +} + +function makeRequest(tableId = 'tbl_1', rowId = 'row_1', groupId = 'grp_1') { + const req = new NextRequest( + `http://localhost:3000/api/table/${tableId}/rows/${rowId}/enrichment/${groupId}` + ) + return GET(req, { params: Promise.resolve({ tableId, rowId, groupId }) }) +} + +const detail: EnrichmentRunDetail = { + startedAt: '2026-06-18T00:00:00.000Z', + completedAt: '2026-06-18T00:00:01.000Z', + durationMs: 1000, + totalCost: 0.05, + matchedProvider: 'hunter', + aborted: false, + providers: [ + { + id: 'hunter', + label: 'Hunter', + toolId: 'hunter_find_email', + status: 'matched', + cost: 0.05, + durationMs: 1000, + error: null, + }, + ], +} + +describe('GET /api/table/[tableId]/rows/[rowId]/enrichment/[groupId]', () => { + beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'session', + }) + mockCheckAccess.mockResolvedValue({ ok: true, table: buildTable() }) + }) + + it('returns the enrichment detail', async () => { + mockLoadEnrichmentDetail.mockResolvedValue(detail) + const res = await makeRequest() + expect(res.status).toBe(200) + const json = await res.json() + expect(json).toEqual({ success: true, data: { detail } }) + expect(mockLoadEnrichmentDetail).toHaveBeenCalledWith( + expect.anything(), + 'tbl_1', + 'row_1', + 'grp_1' + ) + }) + + it('returns null when there is no recorded run', async () => { + mockLoadEnrichmentDetail.mockResolvedValue(null) + const res = await makeRequest() + expect(res.status).toBe(200) + const json = await res.json() + expect(json).toEqual({ success: true, data: { detail: null } }) + }) + + it('401s when unauthenticated', async () => { + hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ success: false }) + const res = await makeRequest() + expect(res.status).toBe(401) + expect(mockLoadEnrichmentDetail).not.toHaveBeenCalled() + }) + + it('denies when access check fails', async () => { + mockCheckAccess.mockResolvedValue({ ok: false, status: 403 }) + const res = await makeRequest() + expect(res.status).toBe(403) + expect(mockLoadEnrichmentDetail).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.ts b/apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.ts new file mode 100644 index 00000000000..34a045f7677 --- /dev/null +++ b/apps/sim/app/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]/route.ts @@ -0,0 +1,51 @@ +import { db } from '@sim/db' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { getEnrichmentDetailContract } from '@/lib/api/contracts/tables' +import { parseRequest } from '@/lib/api/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { loadEnrichmentDetail } from '@/lib/table/rows/executions' +import { accessError, checkAccess } from '@/app/api/table/utils' + +const logger = createLogger('EnrichmentDetailAPI') + +interface RouteParams { + params: Promise<{ tableId: string; rowId: string; groupId: string }> +} + +/** + * GET /api/table/[tableId]/rows/[rowId]/enrichment/[groupId] + * + * Returns the enrichment cascade breakdown (provider outcomes, cost, timing) + * for one enrichment cell. Read on demand by the enrichment details panel — + * this data is deliberately kept off the hot grid read. Returns `null` for + * cells with no recorded run or runs that predate the feature. + */ +export const GET = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => { + const requestId = generateRequestId() + + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + const parsed = await parseRequest(getEnrichmentDetailContract, request, { params }) + if (!parsed.success) return parsed.response + const { tableId, rowId, groupId } = parsed.data.params + + const result = await checkAccess(tableId, authResult.userId, 'read') + if (!result.ok) return accessError(result, requestId, tableId) + + const detail = await loadEnrichmentDetail(db, tableId, rowId, groupId) + + logger.info(`[${requestId}] Loaded enrichment detail`, { + tableId, + rowId, + groupId, + hasDetail: detail !== null, + }) + + return NextResponse.json({ success: true, data: { detail } }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index fc4030998e8..f1f9fe02558 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -32,7 +32,7 @@ import { import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' import { - DEFAULT_BLOCK_COLOR, + adjustBgForContrast, formatCostAmount, formatTokenCount, formatTps, @@ -41,6 +41,7 @@ import { getDisplayName, hasErrorInTree, hasUnhandledErrorInTree, + iconColorClass, isIterationType, parseTime, } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' @@ -119,31 +120,6 @@ function getDisplayChildren(span: TraceSpan): TraceSpan[] { return kids } -/** Returns 'text-white' for dark backgrounds, dark text for light ones. */ -function iconColorClass(bgColor: string): string { - const hex = bgColor.replace('#', '') - if (hex.length !== 6) return 'text-white' - const r = Number.parseInt(hex.slice(0, 2), 16) - const g = Number.parseInt(hex.slice(2, 4), 16) - const b = Number.parseInt(hex.slice(4, 6), 16) - return r * 299 + g * 587 + b * 114 > 160_000 ? 'text-[#111111]' : 'text-white' -} - -/** - * Near-black bgColors disappear against the dark-mode surface (--bg: #1b1b1b). - * Below the luminance threshold we fall back to the neutral block color used - * for blocks with no distinct identity; everything brighter passes through. - */ -function adjustBgForContrast(bgColor: string): string { - const hex = bgColor.replace('#', '') - if (hex.length !== 6) return bgColor - const r = Number.parseInt(hex.slice(0, 2), 16) - const g = Number.parseInt(hex.slice(2, 4), 16) - const b = Number.parseInt(hex.slice(4, 6), 16) - if (r * 299 + g * 587 + b * 114 < 30_000) return DEFAULT_BLOCK_COLOR - return bgColor -} - /** * Flattens the visible (expanded) span tree into a linear list for keyboard * navigation, carrying depth, the chain of parent ids for indent drawing, and diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index 16548e2d3c4..2fa855ae7cc 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -81,6 +81,31 @@ export function getBlockIconAndColor( return { icon: null, bgColor: DEFAULT_BLOCK_COLOR } } +/** Returns 'text-white' for dark backgrounds, dark text for light ones. */ +export function iconColorClass(bgColor: string): string { + const hex = bgColor.replace('#', '') + if (hex.length !== 6) return 'text-white' + const r = Number.parseInt(hex.slice(0, 2), 16) + const g = Number.parseInt(hex.slice(2, 4), 16) + const b = Number.parseInt(hex.slice(4, 6), 16) + return r * 299 + g * 587 + b * 114 > 160_000 ? 'text-[#111111]' : 'text-white' +} + +/** + * Near-black bgColors disappear against the dark-mode surface (--bg: #1b1b1b). + * Below the luminance threshold we fall back to the neutral block color used + * for blocks with no distinct identity; everything brighter passes through. + */ +export function adjustBgForContrast(bgColor: string): string { + const hex = bgColor.replace('#', '') + if (hex.length !== 6) return bgColor + const r = Number.parseInt(hex.slice(0, 2), 16) + const g = Number.parseInt(hex.slice(2, 4), 16) + const b = Number.parseInt(hex.slice(4, 6), 16) + if (r * 299 + g * 587 + b * 114 < 30_000) return DEFAULT_BLOCK_COLOR + return bgColor +} + export function parseTime(value?: string | number | null): number { if (!value) return 0 const ms = typeof value === 'number' ? value : new Date(value).getTime() diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx new file mode 100644 index 00000000000..559d8858cbf --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx @@ -0,0 +1,389 @@ +'use client' + +import { useEffect, useState } from 'react' +import { formatDuration } from '@sim/utils/formatting' +import { Badge, Button, ChipModalTabs, X } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import type { EnrichmentProviderOutcome, EnrichmentRunDetail } from '@/lib/table' +import { + adjustBgForContrast, + getBlockIconAndColor, + iconColorClass, +} from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' +import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks' +import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils' +import { useEnrichmentDetail } from '@/hooks/queries/tables' +import { formatCost } from '@/providers/utils' +import { useLogDetailsUIStore } from '@/stores/logs/store' +import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils' + +type EnrichmentDetailsTab = 'result' | 'cascade' + +type ResultStatus = 'matched' | 'no_match' | 'error' | 'not_run' | 'cancelled' + +const RESULT_STATUS_CONFIG: Record< + ResultStatus, + { variant: React.ComponentProps['variant']; label: string } +> = { + matched: { variant: 'green', label: 'Matched' }, + no_match: { variant: 'gray', label: 'No match' }, + error: { variant: 'red', label: 'Error' }, + not_run: { variant: 'gray', label: 'Not run' }, + cancelled: { variant: 'orange', label: 'Cancelled' }, +} + +/** Minimum bar width so a sub-millisecond provider still shows on the timeline. */ +const MIN_BAR_PCT = 0.5 + +const PROVIDER_STATUS_LABEL: Record = { + matched: 'Matched', + no_match: 'No match', + skipped: 'Skipped', + error: 'Error', + not_run: 'Not run', +} + +interface CascadeRow { + outcome: EnrichmentProviderOutcome + offsetPct: number + widthPct: number +} + +/** + * Lays the (sequential) provider attempts on one timeline so the Cascade tab + * reads like the execution trace waterfall: each bar's offset is the time before + * it ran, its width its own duration. Skipped providers (0ms) get no bar. + */ +function buildCascadeRows(providers: EnrichmentProviderOutcome[]): CascadeRow[] { + const total = Math.max( + 1, + providers.reduce((sum, p) => sum + p.durationMs, 0) + ) + let cursor = 0 + return providers.map((outcome) => { + const offsetMs = cursor + cursor += outcome.durationMs + const offsetPct = Math.min(100 - MIN_BAR_PCT, (offsetMs / total) * 100) + const rawWidth = (outcome.durationMs / total) * 100 + const widthPct = + outcome.durationMs > 0 ? Math.max(MIN_BAR_PCT, Math.min(100 - offsetPct, rawWidth)) : 0 + return { outcome, offsetPct, widthPct } + }) +} + +/** A provider that actually executed its tool (not skipped / never reached). */ +function didRun(p: EnrichmentProviderOutcome): boolean { + return p.status !== 'skipped' && p.status !== 'not_run' +} + +/** + * Derives the cell-level outcome from the cascade — mirrors the executor: a + * cancelled run is `cancelled` regardless of how far the cascade got; otherwise + * `error` only when every provider that ran errored, `not_run` when nothing + * executed (missing inputs), else a clean `no_match`. + */ +function deriveResultStatus(detail: EnrichmentRunDetail): ResultStatus { + if (detail.aborted) return 'cancelled' + if (detail.matchedProvider) return 'matched' + const ran = detail.providers.filter(didRun) + if (ran.length === 0) return 'not_run' + if (ran.every((p) => p.status === 'error')) return 'error' + return 'no_match' +} + +interface DetailRowProps { + label: string + children: React.ReactNode +} + +function DetailRow({ label, children }: DetailRowProps) { + return ( +
+ + {label} + + + {children} + +
+ ) +} + +interface EnrichmentDetailsContentProps { + tableId: string + rowId: string + groupId: string + groupName?: string + isOpen: boolean +} + +function EnrichmentDetailsContent({ + tableId, + rowId, + groupId, + groupName, + isOpen, +}: EnrichmentDetailsContentProps) { + const [activeTab, setActiveTab] = useState('result') + const [prevKey, setPrevKey] = useState(`${rowId}:${groupId}`) + + const key = `${rowId}:${groupId}` + if (prevKey !== key) { + setPrevKey(key) + setActiveTab('result') + } + + const { data: detail, isLoading } = useEnrichmentDetail(tableId, rowId, groupId, { + enabled: isOpen, + }) + + const matchedLabel = detail?.matchedProvider + ? (detail.providers.find((p) => p.id === detail.matchedProvider)?.label ?? + detail.matchedProvider) + : null + const ranCount = detail ? detail.providers.filter(didRun).length : 0 + const lastError = detail + ? [...detail.providers].reverse().find((p) => p.status === 'error')?.error + : null + const timestamp = detail ? formatDate(detail.completedAt) : null + + return ( +
+ setActiveTab(v as EnrichmentDetailsTab)} + /> + + {isLoading ? ( +
+ Loading… +
+ ) : !detail ? ( +
+ + No enrichment details for this run + +
+ ) : activeTab === 'result' ? ( +
+
+
+
+ + Timestamp + + + {timestamp ? `${timestamp.compactDate} ${timestamp.compactTime}` : '—'} + +
+
+ + Enrichment + + + {groupName || 'Enrichment'} + +
+
+ +
+ + + {RESULT_STATUS_CONFIG[deriveResultStatus(detail)].label} + + + + {formatDuration(detail.durationMs, { precision: 2 }) || '—'} + + {formatCost(detail.totalCost)} + {matchedLabel || '—'} + {ranCount} +
+ + {lastError && ( +
+ Error +

{lastError}

+
+ )} +
+
+ ) : ( +
+ {/* Summary strip — mirrors the trace header */} +
+ + {RESULT_STATUS_CONFIG[deriveResultStatus(detail)].label} + + + {formatDuration(detail.durationMs, { precision: 2 }) || '—'} + + + {ranCount} {ranCount === 1 ? 'provider' : 'providers'} + + {detail.totalCost > 0 && ( + + {formatCost(detail.totalCost)} + + )} +
+ + {/* Provider waterfall — each row is one cascade attempt on a shared timeline */} +
+ {buildCascadeRows(detail.providers).map(({ outcome, offsetPct, widthPct }) => { + const ran = didRun(outcome) + const { icon: ProviderIcon, bgColor: rawBgColor } = getBlockIconAndColor( + 'tool', + outcome.toolId + ) + const bgColor = adjustBgForContrast(rawBgColor) + return ( +
+
+
+ {ProviderIcon && ( + + )} +
+ + {outcome.label} + + + {PROVIDER_STATUS_LABEL[outcome.status]} + + {outcome.cost > 0 && ( + + {formatCost(outcome.cost)} + + )} + {ran && ( + + {formatDuration(outcome.durationMs, { precision: 2 }) || '—'} + + )} +
+
+
+ {widthPct > 0 && ( +
+ )} +
+
+ {outcome.error && ( +

+ {outcome.error} +

+ )} +
+ ) + })} +
+
+ )} +
+ ) +} + +interface EnrichmentDetailsProps { + tableId: string + rowId: string | null + groupId: string | null + groupName?: string + isOpen: boolean + onClose: () => void +} + +/** + * Right-edge slideout showing an enrichment cell's run: a Result tab (status, + * duration, total cost, matched provider) and a Cascade tab (per-provider + * outcomes). Mirrors the log-details shell — resizable with a shared persisted + * width — minus the prev/next navigation, which is meaningless for a cell. + */ +export function EnrichmentDetails({ + tableId, + rowId, + groupId, + groupName, + isOpen, + onClose, +}: EnrichmentDetailsProps) { + const panelWidth = useLogDetailsUIStore((state) => state.panelWidth) + const { handleMouseDown } = useLogDetailsResize() + + const maxVw = `${MAX_LOG_DETAILS_WIDTH_RATIO * 100}vw` + const effectiveWidth = `clamp(min(${MIN_LOG_DETAILS_WIDTH}px, ${maxVw}), ${panelWidth}px, ${maxVw})` + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) onClose() + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isOpen, onClose]) + + return ( + <> + {isOpen && ( +
+ )} + +
+ {rowId && groupId && ( +
+
+

Enrichment Details

+ +
+ + +
+ )} +
+ + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/index.ts new file mode 100644 index 00000000000..30bfe18a87b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/index.ts @@ -0,0 +1 @@ +export { EnrichmentDetails } from './enrichment-details' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts index 02d4710b130..d458df4c60a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts @@ -1,5 +1,6 @@ export * from './column-config-sidebar' export * from './context-menu' +export * from './enrichment-details' export * from './enrichments-sidebar' export * from './new-column-dropdown' export * from './row-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index c69b7978747..1dc09fdfd51 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -127,6 +127,10 @@ export interface SelectionSnapshot { /** True iff the exec is in a state that produced a server log * (completed / error / running). Drives the View execution button. */ canViewExecution: boolean + /** True iff this is an enrichment group with a terminal run (completed / + * error) — drives "View execution" opening the enrichment details panel + * instead of a workflow execution log. */ + canViewEnrichment: boolean } | null } @@ -153,6 +157,8 @@ interface TableGridProps { /** Open the enrichments slideout in edit mode for an existing enrichment group. */ onOpenEnrichmentConfig: (group: WorkflowGroup) => void onOpenExecutionDetails: (executionId: string) => void + /** Open the enrichment details panel (cost + provider cascade) for a cell. */ + onOpenEnrichmentDetails: (rowId: string, groupId: string) => void /** Open the row-edit modal for `row`. Wrapper renders the modal. */ onOpenRowModal: (row: TableRowType) => void /** Open the row-delete modal for `snapshots`. Wrapper renders the modal. */ @@ -283,6 +289,7 @@ export function TableGrid({ onOpenEnrichments, onOpenEnrichmentConfig, onOpenExecutionDetails, + onOpenEnrichmentDetails, onOpenRowModal, onRequestDeleteRows, onRequestDeleteAllByFilter, @@ -1005,6 +1012,9 @@ export function TableGrid({ let contextMenuExecutionId: string | null = null let contextMenuIsWorkflowColumn = false let contextMenuHasStartedRun = false + // The (rowId, groupId) of the right-clicked enrichment cell when it has a + // terminal run — drives "View execution" opening the enrichment details panel. + let contextMenuEnrichment: { rowId: string; groupId: string } | null = null // The workflow group of the right-clicked cell, when it's a workflow-output // column. Scopes the run/re-run menu items to just that cell's group (the // cascade re-runs dependents on its own) instead of every group on the row. @@ -1025,7 +1035,8 @@ export function TableGrid({ _exec?.status === 'pending' && typeof _exec?.jobId === 'string' && _exec.jobId.startsWith('paused-') - // Enrichment cells have no workflow execution trace to open. + // Enrichment cells have no workflow execution trace; a terminal run opens + // the enrichment details panel instead. const _isEnrichmentGroup = workflowGroupById.get(_gid)?.type === 'enrichment' contextMenuHasStartedRun = !_isEnrichmentGroup && @@ -1034,10 +1045,22 @@ export function TableGrid({ _exec?.status === 'running' || _isPaused) contextMenuExecutionId = _exec?.executionId ?? null + if ( + _isEnrichmentGroup && + (_exec?.status === 'completed' || _exec?.status === 'error') && + contextMenu.row + ) { + contextMenuEnrichment = { rowId: contextMenu.row.id, groupId: _gid } + } } } function handleViewExecution() { + if (contextMenuEnrichment) { + onOpenEnrichmentDetails(contextMenuEnrichment.rowId, contextMenuEnrichment.groupId) + closeContextMenu() + return + } if (!contextMenuExecutionId) return onOpenExecutionDetails(contextMenuExecutionId) closeContextMenu() @@ -3369,8 +3392,8 @@ export function TableGrid({ // running/completed/error. const isPaused = status === 'pending' && typeof exec?.jobId === 'string' && exec.jobId.startsWith('paused-') - // Enrichment groups have no workflow execution to open — never offer "View - // execution" for them. + // Enrichment groups have no workflow execution / trace; instead a terminal + // run exposes the enrichment details panel (cost + provider cascade). const isEnrichmentGroup = workflowGroupById.get(groupId)?.type === 'enrichment' return { rowId: row.id, @@ -3383,6 +3406,7 @@ export function TableGrid({ !isEnrichmentGroup && Boolean(exec?.executionId) && (status === 'completed' || status === 'error' || status === 'running' || isPaused), + canViewEnrichment: isEnrichmentGroup && (status === 'completed' || status === 'error'), } }, [normalizedSelection, rows, displayColumns, workflowGroupById]) @@ -3474,7 +3498,8 @@ export function TableGrid({ prev.singleWorkflowCell.rowId === singleWorkflowCell.rowId && prev.singleWorkflowCell.groupId === singleWorkflowCell.groupId && prev.singleWorkflowCell.executionId === singleWorkflowCell.executionId && - prev.singleWorkflowCell.canViewExecution === singleWorkflowCell.canViewExecution + prev.singleWorkflowCell.canViewExecution === singleWorkflowCell.canViewExecution && + prev.singleWorkflowCell.canViewEnrichment === singleWorkflowCell.canViewEnrichment const sameRunScope = (prev?.selectedRunScope ?? null) === null && selectedRunScope === null ? true @@ -3879,7 +3904,10 @@ export function TableGrid({ onInsertBelow={handleInsertRowBelow} onDuplicate={handleDuplicateRow} onViewExecution={handleViewExecution} - canViewExecution={Boolean(contextMenuExecutionId) && contextMenuHasStartedRun} + canViewExecution={ + (Boolean(contextMenuExecutionId) && contextMenuHasStartedRun) || + Boolean(contextMenuEnrichment) + } canEditCell={!contextMenuIsWorkflowColumn} selectedRowCount={selectedRowCount} onRunWorkflows={ diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index ffcc2b33763..5d9a0660bf6 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -38,6 +38,7 @@ import type { DeletedRowSnapshot } from '@/stores/table/types' import { type ColumnConfig, ColumnConfigSidebar, + EnrichmentDetails, EnrichmentsSidebar, NewColumnDropdown, RowModal, @@ -78,12 +79,14 @@ type SlideoutState = | { kind: 'enrichments'; editGroup?: WorkflowGroup } | { kind: 'workflow'; config: WorkflowConfig } | { kind: 'execution'; executionId: string } + | { kind: 'enrichment-details'; rowId: string; groupId: string } type SlideoutAction = | { type: 'OPEN_COLUMN'; config: ColumnConfig } | { type: 'OPEN_ENRICHMENTS'; editGroup?: WorkflowGroup } | { type: 'OPEN_WORKFLOW'; config: WorkflowConfig } | { type: 'OPEN_EXECUTION'; executionId: string } + | { type: 'OPEN_ENRICHMENT_DETAILS'; rowId: string; groupId: string } | { type: 'CLOSE' } function slideoutReducer(_state: SlideoutState, action: SlideoutAction): SlideoutState { @@ -96,6 +99,8 @@ function slideoutReducer(_state: SlideoutState, action: SlideoutAction): Slideou return { kind: 'workflow', config: action.config } case 'OPEN_EXECUTION': return { kind: 'execution', executionId: action.executionId } + case 'OPEN_ENRICHMENT_DETAILS': + return { kind: 'enrichment-details', rowId: action.rowId, groupId: action.groupId } case 'CLOSE': return { kind: 'none' } } @@ -176,6 +181,9 @@ export function Table({ const onOpenExecutionDetails = useCallback((executionId: string) => { dispatch({ type: 'OPEN_EXECUTION', executionId }) }, []) + const onOpenEnrichmentDetails = useCallback((rowId: string, groupId: string) => { + dispatch({ type: 'OPEN_ENRICHMENT_DETAILS', rowId, groupId }) + }, []) const onCloseSlideout = () => dispatch({ type: 'CLOSE' }) const onOpenRowModal = (row: TableRowType) => setEditingRow(row) // useCallback because is memo-wrapped — these flow into @@ -565,7 +573,7 @@ export function Table({ const sidebarReservedPx = slideout.kind === 'column' || slideout.kind === 'workflow' || slideout.kind === 'enrichments' ? COLUMN_SIDEBAR_WIDTH - : slideout.kind === 'execution' + : slideout.kind === 'execution' || slideout.kind === 'enrichment-details' ? logPanelWidth : 0 @@ -592,6 +600,10 @@ export function Table({ const columnConfig = slideout.kind === 'column' ? slideout.config : null const workflowConfig = slideout.kind === 'workflow' ? slideout.config : null const executionId = slideout.kind === 'execution' ? slideout.executionId : null + const enrichmentDetailsTarget = slideout.kind === 'enrichment-details' ? slideout : null + const enrichmentDetailsGroupName = + enrichmentDetailsTarget && + tableWorkflowGroups.find((g) => g.id === enrichmentDetailsTarget.groupId)?.name // Fetch the workflow log when the execution-details slideout is open. Reuses // the logs page's directly — no intermediate wrapper needed for // a one-line query forward. @@ -674,6 +686,7 @@ export function Table({ onOpenEnrichments={onOpenEnrichments} onOpenEnrichmentConfig={onOpenEnrichmentConfig} onOpenExecutionDetails={onOpenExecutionDetails} + onOpenEnrichmentDetails={onOpenEnrichmentDetails} onOpenRowModal={onOpenRowModal} onRequestDeleteRows={onRequestDeleteRows} onRequestDeleteAllByFilter={onRequestDeleteAllByFilter} @@ -746,7 +759,12 @@ export function Table({ const id = selection.singleWorkflowCell?.executionId if (id) onOpenExecutionDetails(id) } - : undefined + : selection.singleWorkflowCell?.canViewEnrichment + ? () => { + const cell = selection.singleWorkflowCell + if (cell) onOpenEnrichmentDetails(cell.rowId, cell.groupId) + } + : undefined } /> )} @@ -785,6 +803,14 @@ export function Table({ isOpen={Boolean(executionId)} onClose={onCloseSlideout} /> + {tableData && ( ({ mockExecuteTool: vi.fn() })) +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +import { runEnrichment, skippedEnrichmentDetail } from '@/enrichments/run' +import type { EnrichmentConfig, EnrichmentProvider } from '@/enrichments/types' + +const ICON = (() => null) as unknown as EnrichmentConfig['icon'] + +function prov( + id: string, + opts: { + build?: (inputs: Record) => Record | null + map?: (output: Record) => Record | null + } = {} +): EnrichmentProvider { + return { + id, + label: id.toUpperCase(), + toolId: `tool_${id}`, + buildParams: opts.build ?? (() => ({ q: 'x' })), + mapOutput: opts.map ?? ((o) => (o.email ? { email: o.email } : null)), + } +} + +function config(providers: EnrichmentProvider[]): EnrichmentConfig { + return { + id: 'test', + name: 'Test', + description: '', + icon: ICON, + inputs: [], + outputs: [], + providers, + } +} + +const ctx = { workspaceId: 'ws-1' } + +beforeEach(() => { + mockExecuteTool.mockReset() +}) + +describe('runEnrichment cascade detail', () => { + it('records the first match and stops the cascade', async () => { + mockExecuteTool.mockImplementation((toolId: string) => { + if (toolId === 'tool_a') return { success: false, output: { status: 404 } } + if (toolId === 'tool_b') + return { success: true, output: { email: 'j@acme.com', cost: { total: 0.05 } } } + throw new Error('tool_c should never run after a match') + }) + + const outcome = await runEnrichment(config([prov('a'), prov('b'), prov('c')]), {}, ctx) + + expect(outcome.result).toEqual({ email: 'j@acme.com' }) + expect(outcome.cost).toBe(0.05) + expect(outcome.error).toBeNull() + expect(outcome.provider).toBe('B') + + expect(outcome.detail.matchedProvider).toBe('b') + expect(outcome.detail.totalCost).toBe(0.05) + // The full cascade is recorded; the provider after the match is `not_run`. + expect(outcome.detail.providers.map((p) => p.id)).toEqual(['a', 'b', 'c']) + expect(outcome.detail.providers.map((p) => p.status)).toEqual([ + 'no_match', + 'matched', + 'not_run', + ]) + expect(outcome.detail.providers[1]?.cost).toBe(0.05) + expect(outcome.detail.providers.every((p) => typeof p.durationMs === 'number')).toBe(true) + // The tool is never called for the matched-past provider. + expect(mockExecuteTool).toHaveBeenCalledTimes(2) + }) + + it('marks providers with insufficient inputs as skipped without calling the tool', async () => { + mockExecuteTool.mockImplementation(() => ({ + success: true, + output: { email: 'j@acme.com' }, + })) + + const outcome = await runEnrichment( + config([prov('a', { build: () => null }), prov('b')]), + {}, + ctx + ) + + expect(outcome.detail.providers[0]).toMatchObject({ id: 'a', status: 'skipped', durationMs: 0 }) + expect(outcome.detail.providers[1]?.status).toBe('matched') + // Only provider b actually called the tool. + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + }) + + it('sets error only when every provider that ran errored', async () => { + mockExecuteTool.mockImplementation(() => ({ success: false, output: { status: 500 } })) + + const outcome = await runEnrichment(config([prov('a'), prov('b')]), {}, ctx) + + expect(outcome.result).toEqual({}) + expect(outcome.error).not.toBeNull() + expect(outcome.provider).toBeNull() + expect(outcome.detail.matchedProvider).toBeNull() + expect(outcome.detail.providers.map((p) => p.status)).toEqual(['error', 'error']) + expect(outcome.detail.providers.every((p) => p.error)).toBe(true) + }) + + it('treats a clean miss (ran, empty result) as no_match with no error', async () => { + mockExecuteTool.mockImplementation(() => ({ success: true, output: {} })) + + const outcome = await runEnrichment(config([prov('a')]), {}, ctx) + + expect(outcome.result).toEqual({}) + expect(outcome.error).toBeNull() + expect(outcome.detail.providers.map((p) => p.status)).toEqual(['no_match']) + }) + + it('skippedEnrichmentDetail marks every provider skipped without running', () => { + const detail = skippedEnrichmentDetail(config([prov('a'), prov('b')])) + expect(detail.matchedProvider).toBeNull() + expect(detail.totalCost).toBe(0) + expect(detail.providers.map((p) => p.status)).toEqual(['skipped', 'skipped']) + expect(mockExecuteTool).not.toHaveBeenCalled() + }) + + it('marks unattempted providers not_run when the signal is already aborted', async () => { + const controller = new AbortController() + controller.abort() + const outcome = await runEnrichment( + config([prov('a'), prov('b')]), + {}, + { + ...ctx, + signal: controller.signal, + } + ) + expect(mockExecuteTool).not.toHaveBeenCalled() + expect(outcome.detail.aborted).toBe(true) + expect(outcome.detail.providers.map((p) => p.status)).toEqual(['not_run', 'not_run']) + }) + + it('does not error when some providers no-match and only some error', async () => { + mockExecuteTool.mockImplementation((toolId: string) => { + if (toolId === 'tool_a') return { success: false, output: { status: 500 } } + return { success: false, output: { status: 404 } } + }) + + const outcome = await runEnrichment(config([prov('a'), prov('b')]), {}, ctx) + + expect(outcome.error).toBeNull() + expect(outcome.detail.providers.map((p) => p.status)).toEqual(['error', 'no_match']) + }) +}) diff --git a/apps/sim/enrichments/run.ts b/apps/sim/enrichments/run.ts index 5b9a16fadb7..03d1af91bbb 100644 --- a/apps/sim/enrichments/run.ts +++ b/apps/sim/enrichments/run.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import type { EnrichmentProviderOutcome, EnrichmentRunDetail } from '@/lib/table/types' import type { EnrichmentConfig, EnrichmentRunContext } from '@/enrichments/types' import { executeTool } from '@/tools' @@ -19,6 +20,38 @@ export interface EnrichmentRunOutcome { error: string | null /** Label of the provider whose result was returned, or `null` on no match. */ provider: string | null + /** Per-provider cascade breakdown + timing for the enrichment details panel. */ + detail: EnrichmentRunDetail +} + +/** + * Detail for a terminal cell that recorded no provider attempt — missing + * required inputs, or cancelled before any provider ran. Every provider is + * marked `skipped` so the details panel stays informative (shows the configured + * cascade) instead of empty. + */ +export function skippedEnrichmentDetail( + enrichment: EnrichmentConfig, + opts: { aborted?: boolean } = {} +): EnrichmentRunDetail { + const now = new Date().toISOString() + return { + startedAt: now, + completedAt: now, + durationMs: 0, + totalCost: 0, + matchedProvider: null, + aborted: opts.aborted ?? false, + providers: enrichment.providers.map((provider) => ({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'skipped' as const, + cost: 0, + durationMs: 0, + error: null, + })), + } } /** True when at least one output value in the result is non-empty. */ @@ -53,12 +86,29 @@ export async function runEnrichment( let ranCount = 0 let errorCount = 0 let lastError: string | null = null + let matchedProvider: string | null = null + let winner: { result: Record; label: string } | null = null + const providers: EnrichmentProviderOutcome[] = [] + const startedAt = Date.now() - for (const provider of enrichment.providers) { + for (let i = 0; i < enrichment.providers.length; i++) { + const provider = enrichment.providers[i] if (ctx.signal?.aborted) break const params = provider.buildParams(inputs) - if (!params) continue + if (!params) { + providers.push({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'skipped', + cost: 0, + durationMs: 0, + error: null, + }) + continue + } ranCount++ + const providerStart = Date.now() try { const response = await executeTool( provider.toolId, @@ -72,18 +122,60 @@ export async function runEnrichment( // found" rather than an error). Other statuses (auth, rate-limit, 5xx) // are real errors and propagate. const status = (response.output as { status?: unknown } | undefined)?.status - if (status === 404) continue + if (status === 404) { + providers.push({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'no_match', + cost: 0, + durationMs: Date.now() - providerStart, + error: null, + }) + continue + } throw new Error(response.error ?? `${provider.toolId} failed`) } - cost += readCost(response.output) + const providerCost = readCost(response.output) + cost += providerCost const result = provider.mapOutput(response.output) if (result && hasResult(result)) { + providers.push({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'matched', + cost: providerCost, + durationMs: Date.now() - providerStart, + error: null, + }) + matchedProvider = provider.id + winner = { result, label: provider.label } logger.info('Enrichment hit', { enrichmentId: enrichment.id, provider: provider.id }) - return { result, cost, error: null, provider: provider.label } + break } + // Ran cleanly but mapped to nothing — a no-match, fall through to the next. + providers.push({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'no_match', + cost: providerCost, + durationMs: Date.now() - providerStart, + error: null, + }) } catch (err) { errorCount++ lastError = getErrorMessage(err) + providers.push({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'error', + cost: 0, + durationMs: Date.now() - providerStart, + error: lastError, + }) logger.warn('Enrichment provider failed; trying next', { enrichmentId: enrichment.id, provider: provider.id, @@ -92,8 +184,40 @@ export async function runEnrichment( } } + // Any provider not represented yet never ran — the cascade short-circuited on + // a match or aborted mid-run. Record them as `not_run` (in registry order) so + // the panel always shows the full configured cascade. + const seen = new Set(providers.map((p) => p.id)) + for (const provider of enrichment.providers) { + if (seen.has(provider.id)) continue + providers.push({ + id: provider.id, + label: provider.label, + toolId: provider.toolId, + status: 'not_run', + cost: 0, + durationMs: 0, + error: null, + }) + } + + const completedAt = Date.now() + const detail: EnrichmentRunDetail = { + startedAt: new Date(startedAt).toISOString(), + completedAt: new Date(completedAt).toISOString(), + durationMs: completedAt - startedAt, + totalCost: cost, + matchedProvider, + aborted: Boolean(ctx.signal?.aborted), + providers, + } + + if (winner) { + return { result: winner.result, cost, error: null, provider: winner.label, detail } + } + // No provider hit. Surface an error only when every provider that ran errored // (infra/auth/rate-limit) — a clean miss returns a blank result instead. const error = ranCount > 0 && errorCount === ranCount ? lastError : null - return { result: {}, cost, error, provider: null } + return { result: {}, cost, error, provider: null, detail } } diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 4bc2a19ce33..05933a76e2c 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -44,6 +44,7 @@ import { exportDownloadContract, exportTableAsyncContract, findTableRowsContract, + getEnrichmentDetailContract, getTableContract, type InsertTableRowBodyInput, importIntoTableAsyncContract, @@ -72,6 +73,7 @@ import { } from '@/lib/api/contracts/tables' import type { CsvHeaderMapping, + EnrichmentRunDetail, Filter, RowData, RowExecutionMetadata, @@ -115,6 +117,10 @@ export const tableKeys = { [...tableKeys.rowsRoot(tableId), 'find', paramsKey] as const, activeDispatches: (tableId: string) => [...tableKeys.detail(tableId), 'active-dispatches'] as const, + enrichmentDetails: (tableId: string) => + [...tableKeys.detail(tableId), 'enrichment-detail'] as const, + enrichmentDetail: (tableId: string, rowId: string, groupId: string) => + [...tableKeys.enrichmentDetails(tableId), rowId, groupId] as const, } type TableRowsParams = Omit & @@ -297,6 +303,44 @@ async function fetchTableRunState(tableId: string, signal?: AbortSignal): Promis } } +async function fetchEnrichmentDetail( + tableId: string, + rowId: string, + groupId: string, + signal?: AbortSignal +): Promise { + const response = await requestJson(getEnrichmentDetailContract, { + params: { tableId, rowId, groupId }, + signal, + }) + return response.data.detail +} + +/** + * Enrichment cascade breakdown for one cell, fetched on demand when the + * enrichment details panel opens. Kept off the hot grid read — only queried + * while `enabled` (panel open with a selected row + group). + * + * `staleTime: 0` so reopening the panel always refetches: a cell can be re-run + * between opens (the run writes new `enrichmentDetails` in the background with no + * client invalidation), and the panel is opened on demand, so a fresh fetch per + * open keeps the cascade in sync without a cached stale run. + */ +export function useEnrichmentDetail( + tableId: string, + rowId: string | null, + groupId: string | null, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: tableKeys.enrichmentDetail(tableId, rowId ?? '', groupId ?? ''), + queryFn: ({ signal }) => + fetchEnrichmentDetail(tableId, rowId as string, groupId as string, signal), + enabled: Boolean(tableId && rowId && groupId) && (options?.enabled ?? true), + staleTime: 0, + }) +} + /** Count groups flipped to in-flight (`pending`) by an optimistic schedule that * weren't in-flight before — the delta to add to the run-state counter. */ function countNewlyInFlight(before: RowExecutions, after: RowExecutions): number { diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index 9e89c84078e..b9d0512b0c0 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -3,6 +3,7 @@ import { z } from 'zod' import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types' import type { CsvHeaderMapping, + EnrichmentRunDetail, Filter, RowData, Sort, @@ -905,6 +906,29 @@ export const deleteTableRowContract = defineRouteContract({ }, }) +export const enrichmentDetailParamsSchema = tableRowParamsSchema.extend({ + groupId: z.string().min(1), +}) + +/** + * Per-(row, group) enrichment cascade breakdown. Modeled as a domain object so + * the `EnrichmentRunDetail` TS type stays the single source of truth (matching + * `tableRowSchema` / `tableDefinitionSchema`). `null` when the cell has no + * recorded run or the run predates this feature. + */ +export const getEnrichmentDetailContract = defineRouteContract({ + method: 'GET', + path: '/api/table/[tableId]/rows/[rowId]/enrichment/[groupId]', + params: enrichmentDetailParamsSchema, + response: { + mode: 'json', + schema: successResponseSchema( + z.object({ detail: domainObjectSchema().nullable() }) + ), + }, +}) +export type GetEnrichmentDetailResponse = ContractJsonResponse + export const deleteTableRowsContract = defineRouteContract({ method: 'DELETE', path: '/api/table/[tableId]/rows', diff --git a/apps/sim/lib/table/rows/executions.ts b/apps/sim/lib/table/rows/executions.ts index 5555b856178..17dc9bd4025 100644 --- a/apps/sim/lib/table/rows/executions.ts +++ b/apps/sim/lib/table/rows/executions.ts @@ -11,6 +11,7 @@ import type { DbOrTx } from '@/lib/db/types' import { getColumnId } from '@/lib/table/column-keys' import { areGroupDepsSatisfied } from '@/lib/table/deps' import type { + EnrichmentRunDetail, RowData, RowExecutionMetadata, RowExecutions, @@ -29,8 +30,22 @@ export async function loadExecutionsByRow( const ids = Array.from(new Set(rowIds)) const result = new Map() if (ids.length === 0) return result + // Explicit column list, never `select()` — `enrichmentDetails` is large and + // must stay off the hot grid read path (fetched on demand via + // `loadEnrichmentDetail`). const rows = await trx - .select() + .select({ + rowId: tableRowExecutions.rowId, + groupId: tableRowExecutions.groupId, + status: tableRowExecutions.status, + executionId: tableRowExecutions.executionId, + jobId: tableRowExecutions.jobId, + workflowId: tableRowExecutions.workflowId, + error: tableRowExecutions.error, + runningBlockIds: tableRowExecutions.runningBlockIds, + blockErrors: tableRowExecutions.blockErrors, + cancelledAt: tableRowExecutions.cancelledAt, + }) .from(tableRowExecutions) .where(inArray(tableRowExecutions.rowId, ids)) for (const r of rows) { @@ -61,6 +76,31 @@ export async function loadExecutionsForRow(trx: DbOrTx, rowId: string): Promise< return byRow.get(rowId) ?? {} } +/** + * Loads the enrichment cascade breakdown for one `(tableId, rowId, groupId)`, + * or `null` when there is no exec row or it predates the feature. Read on demand + * by the enrichment details panel — kept off `loadExecutionsByRow`. + */ +export async function loadEnrichmentDetail( + trx: DbOrTx, + tableId: string, + rowId: string, + groupId: string +): Promise { + const [row] = await trx + .select({ enrichmentDetails: tableRowExecutions.enrichmentDetails }) + .from(tableRowExecutions) + .where( + and( + eq(tableRowExecutions.tableId, tableId), + eq(tableRowExecutions.rowId, rowId), + eq(tableRowExecutions.groupId, groupId) + ) as SQL + ) + .limit(1) + return (row?.enrichmentDetails as EnrichmentRunDetail | null | undefined) ?? null +} + /** * Derive automatic clears + cancellation candidates from a row's data patch. * @@ -212,6 +252,7 @@ export async function writeExecutionsPatch( runningBlockIds: value.runningBlockIds ?? [], blockErrors: value.blockErrors ?? {}, cancelledAt: value.cancelledAt ? new Date(value.cancelledAt) : null, + enrichmentDetails: value.enrichmentDetails ?? null, updatedAt: new Date(), } as const @@ -235,6 +276,11 @@ export async function writeExecutionsPatch( runningBlockIds: insertValues.runningBlockIds, blockErrors: insertValues.blockErrors, cancelledAt: insertValues.cancelledAt, + // Sticky: preserve a prior cascade breakdown when this write omits + // it (e.g. the running pickup stamp) so only an explicit detail + // overwrites it. Re-runs delete the row first, so this never serves + // stale detail across runs. + enrichmentDetails: sql`coalesce(excluded.enrichment_details, ${tableRowExecutions.enrichmentDetails})`, updatedAt: insertValues.updatedAt, }, where: and( @@ -269,6 +315,10 @@ export async function writeExecutionsPatch( runningBlockIds: insertValues.runningBlockIds, blockErrors: insertValues.blockErrors, cancelledAt: insertValues.cancelledAt, + // Sticky: preserve a prior cascade breakdown when this write omits it + // (e.g. the running pickup stamp) so only an explicit detail overwrites + // it. Re-runs delete the row first, so this never serves stale detail. + enrichmentDetails: sql`coalesce(excluded.enrichment_details, ${tableRowExecutions.enrichmentDetails})`, updatedAt: insertValues.updatedAt, }, }) diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index cbadfd75c61..ba08af9c83e 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -134,6 +134,59 @@ export interface WorkflowGroup { autoRun?: boolean } +/** + * State of one provider in an enrichment cascade run. `matched`/`no_match`/ + * `error` actually called the tool; `skipped` had insufficient inputs; `not_run` + * was never reached because an earlier provider matched. + */ +export type EnrichmentProviderStatus = 'matched' | 'no_match' | 'skipped' | 'error' | 'not_run' + +/** + * Outcome of one provider attempt in an enrichment cascade, for the enrichment + * details panel. The full configured cascade is recorded: `skipped` providers + * had insufficient inputs, `not_run` providers sit after the match. + */ +export interface EnrichmentProviderOutcome { + /** Provider id, e.g. `'hunter'`. */ + id: string + /** Human label, e.g. `'Hunter'`. */ + label: string + /** Tool id the provider runs, e.g. `'hunter_find_email'` — resolves the block + * icon for the details panel. */ + toolId: string + status: EnrichmentProviderStatus + /** Hosted-key cost (USD) this provider incurred; `0` for skip / no_match / error / BYOK. */ + cost: number + /** Wall-clock ms this provider's tool call took; `0` for skipped. */ + durationMs: number + /** Error message when `status === 'error'`, else `null`. */ + error: string | null +} + +/** + * Per-(row, group) cascade breakdown for an enrichment run, surfaced in the + * enrichment details panel. Persisted on the `tableRowExecutions` sidecar but + * deliberately kept out of the hot grid read path (fetched on demand) — it can + * carry a dozen provider outcomes per cell. + */ +export interface EnrichmentRunDetail { + /** ISO timestamp when the cascade started. */ + startedAt: string + /** ISO timestamp when the cascade finished. */ + completedAt: string + /** Wall-clock ms across the whole cascade. */ + durationMs: number + /** Sum of per-provider hosted-key cost (USD). */ + totalCost: number + /** Provider id that produced the match, or `null` on no match. */ + matchedProvider: string | null + /** True when the run was cancelled (stop / signal abort) — drives a + * "Cancelled" result rather than inferring no-match/not-run from the cascade. */ + aborted: boolean + /** Every configured provider, in cascade order (including `not_run` ones). */ + providers: EnrichmentProviderOutcome[] +} + /** * Per-row execution state for one workflow group, persisted as a row in the * `tableRowExecutions` sidecar keyed by `(rowId, groupId)`. Holds run @@ -163,6 +216,13 @@ export interface RowExecutionMetadata { * re-runs whose `cancelledAt > dispatch.requestedAt` — a user cancel * mid-dispatch must not be overridden by `isManualRun`. */ cancelledAt?: string + /** + * Enrichment cascade breakdown for `enrichment`-type groups, written on the + * terminal cell write. Persisted on `tableRowExecutions` but NOT hydrated by + * `loadExecutionsByRow` (kept off the hot grid read) — read it on demand via + * `loadEnrichmentDetail` for the details panel. + */ + enrichmentDetails?: EnrichmentRunDetail | null } /** Map of `WorkflowGroup.id` → execution state. Stored on every row. */ diff --git a/bun.lock b/bun.lock index fb7f52d232e..df94ab54101 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/packages/db/migrations/0244_table_row_executions_enrichment_details.sql b/packages/db/migrations/0244_table_row_executions_enrichment_details.sql new file mode 100644 index 00000000000..af1efb43af7 --- /dev/null +++ b/packages/db/migrations/0244_table_row_executions_enrichment_details.sql @@ -0,0 +1 @@ +ALTER TABLE "table_row_executions" ADD COLUMN "enrichment_details" jsonb; \ No newline at end of file diff --git a/packages/db/migrations/meta/0244_snapshot.json b/packages/db/migrations/meta/0244_snapshot.json new file mode 100644 index 00000000000..cae6b50eef2 --- /dev/null +++ b/packages/db/migrations/meta/0244_snapshot.json @@ -0,0 +1,16735 @@ +{ + "id": "c5ce36a0-5b8d-4534-ad33-5631beab1130", + "prevId": "dca0a2e7-f031-462f-bda2-d6f52ab4a9ff", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 2dabe616501..c1e8428e8c1 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1702,6 +1702,13 @@ "when": 1781895339512, "tag": "0243_kb_workspace_cascade", "breakpoints": true + }, + { + "idx": 244, + "version": "7", + "when": 1781899910981, + "tag": "0244_table_row_executions_enrichment_details", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index dd93e988d3d..016684b0d17 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -3387,6 +3387,13 @@ export const tableRowExecutions = pgTable( runningBlockIds: text('running_block_ids').array().notNull().default(sql`'{}'::text[]`), blockErrors: jsonb('block_errors').notNull().default({}), cancelledAt: timestamp('cancelled_at'), + /** + * Enrichment cascade breakdown (provider outcomes, cost, timing) for + * `enrichment`-type groups. Null for workflow groups and pre-feature runs. + * Deliberately excluded from the hot grid read (`loadExecutionsByRow`) — read + * on demand for the enrichment details panel. + */ + enrichmentDetails: jsonb('enrichment_details'), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 8ee955cda1c..b294838bac0 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 856, - zodRoutes: 856, + totalRoutes: 857, + zodRoutes: 857, nonZodRoutes: 0, } as const From 5925651cbc85906ae7ab033eaf843044157e4d74 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:12:42 -0700 Subject: [PATCH 12/16] feat(vfs): add lazy vfs + remove dynamic fields for prompt caching hits (#5138) * feat(vfs): add lazy vfs + remove dynamic fields for prompt caching hits * feat(vfs): send typed workspace snapshot for append-only deltas Build the workspace inventory from the primary db (fixes replica-lag staleness) and emit it as a typed VfsSnapshotV1 `vfs` payload alongside the markdown, so the mothership can diff it into append-only baseline/delta messages. Generate the TS contract mirror from the Go-owned JSON schema (sync-vfs-snapshot-contract) and sort connector types so diffs stay byte-stable. * fix(lint): fix lint * fix(vfs): forward the typed snapshot through the branch payload builder The branch buildPayload implementations hand-list the params they pass to buildCopilotRequestPayload and forwarded workspaceContext but dropped vfs, so the typed snapshot never reached the Go request (req.Vfs was always nil and the append-only delta path never engaged). Forward vfs in both the workflow and workspace branches, and add a regression guard asserting the branch threads it through (the bug slipped past tests because post.test mocked the payload builder and payload.test called it directly, bypassing the branch). * improvement(contracts): update vfs contracts --- apps/sim/lib/copilot/chat/payload.ts | 3 + apps/sim/lib/copilot/chat/post.test.ts | 17 +- apps/sim/lib/copilot/chat/post.ts | 18 +- .../copilot/chat/workspace-context.test.ts | 154 ++++++ .../sim/lib/copilot/chat/workspace-context.ts | 297 ++++++++--- apps/sim/lib/copilot/generated/metrics-v1.ts | 2 + .../copilot/generated/trace-attributes-v1.ts | 6 + .../lib/copilot/generated/vfs-snapshot-v1.ts | 131 +++++ apps/sim/lib/copilot/tools/handlers/vfs.ts | 4 +- apps/sim/lib/copilot/vfs/operations.ts | 5 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 480 +++++++++++------- package.json | 2 + scripts/generate-mship-contracts.ts | 1 + scripts/sync-vfs-snapshot-contract.ts | 46 ++ 14 files changed, 886 insertions(+), 280 deletions(-) create mode 100644 apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts create mode 100644 scripts/sync-vfs-snapshot-contract.ts diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index a3d0bb9014b..e718b33cce3 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -3,6 +3,7 @@ import { toError } from '@sim/utils/errors' import { LRUCache } from 'lru-cache' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { isPaid } from '@/lib/billing/plan-helpers' +import type { VfsSnapshotV1 } from '@/lib/copilot/generated/vfs-snapshot-v1' import { getExposedIntegrationTools } from '@/lib/copilot/integration-tools' import { getToolEntry } from '@/lib/copilot/tool-executor/router' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' @@ -33,6 +34,7 @@ interface BuildPayloadParams { prefetch?: boolean implicitFeedback?: string workspaceContext?: string + vfs?: VfsSnapshotV1 userPermission?: string userTimezone?: string userMetadata?: { @@ -366,6 +368,7 @@ export async function buildCopilotRequestPayload( ...(mothershipTools.length > 0 ? { mothershipTools } : {}), ...(commands && commands.length > 0 ? { commands } : {}), ...(params.workspaceContext ? { workspaceContext: params.workspaceContext } : {}), + ...(params.vfs ? { vfs: params.vfs } : {}), ...(params.userPermission ? { userPermission: params.userPermission } : {}), ...(params.userTimezone ? { userTimezone: params.userTimezone } : {}), ...(params.userMetadata && diff --git a/apps/sim/lib/copilot/chat/post.test.ts b/apps/sim/lib/copilot/chat/post.test.ts index 118f6fee775..37e85038e37 100644 --- a/apps/sim/lib/copilot/chat/post.test.ts +++ b/apps/sim/lib/copilot/chat/post.test.ts @@ -17,7 +17,7 @@ const getUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions const { getEffectiveDecryptedEnv, - generateWorkspaceContext, + generateWorkspaceSnapshot, processContextsServer, resolveActiveResourceContext, buildCopilotRequestPayload, @@ -31,7 +31,7 @@ const { mockPublishStatusChanged, } = vi.hoisted(() => ({ getEffectiveDecryptedEnv: vi.fn(), - generateWorkspaceContext: vi.fn(), + generateWorkspaceSnapshot: vi.fn(), processContextsServer: vi.fn(), resolveActiveResourceContext: vi.fn(), buildCopilotRequestPayload: vi.fn(), @@ -56,7 +56,7 @@ vi.mock('@/lib/environment/utils', () => ({ })) vi.mock('@/lib/copilot/chat/workspace-context', () => ({ - generateWorkspaceContext, + generateWorkspaceSnapshot, })) vi.mock('@/lib/copilot/chat/process-contents', () => ({ @@ -142,7 +142,10 @@ describe('handleUnifiedChatPost', () => { }) getUserEntityPermissions.mockResolvedValue('write') getEffectiveDecryptedEnv.mockResolvedValue({ API_KEY: 'secret' }) - generateWorkspaceContext.mockResolvedValue('workspace context') + generateWorkspaceSnapshot.mockResolvedValue({ + markdown: 'workspace context', + snapshot: { workflows: [{ id: 'wf-1', name: 'Alpha', path: 'workflows/Alpha' }] }, + }) processContextsServer.mockResolvedValue([]) resolveActiveResourceContext.mockResolvedValue(null) buildCopilotRequestPayload.mockImplementation(async (params: Record) => params) @@ -178,11 +181,13 @@ describe('handleUnifiedChatPost', () => { ) expect(response.status).toBe(200) - expect(generateWorkspaceContext).toHaveBeenCalledWith('ws-1', 'user-1') + expect(generateWorkspaceSnapshot).toHaveBeenCalledWith('ws-1', 'user-1') expect(buildCopilotRequestPayload).toHaveBeenCalledWith( expect.objectContaining({ model: 'claude-opus-4-8', workspaceContext: 'workspace context', + // Regression guard: the branch must forward the typed snapshot, not drop it. + vfs: expect.objectContaining({ workflows: expect.any(Array) }), }), { selectedModel: 'claude-opus-4-8' } ) @@ -221,6 +226,8 @@ describe('handleUnifiedChatPost', () => { expect.objectContaining({ workspaceId: 'ws-1', workspaceContext: 'workspace context', + // Regression guard: the branch must forward the typed snapshot, not drop it. + vfs: expect.objectContaining({ workflows: expect.any(Array) }), }), { selectedModel: '' } ) diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 1778f80c780..ddd8ba36252 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -22,7 +22,7 @@ import { resolveActiveResourceContext, } from '@/lib/copilot/chat/process-contents' import { finalizeAssistantTurn } from '@/lib/copilot/chat/terminal-state' -import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context' +import { generateWorkspaceSnapshot } from '@/lib/copilot/chat/workspace-context' import { chatPubSub } from '@/lib/copilot/chat-status' import { COPILOT_REQUEST_MODES } from '@/lib/copilot/constants' import { @@ -32,6 +32,7 @@ import { } from '@/lib/copilot/generated/trace-attribute-values-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' +import type { VfsSnapshotV1 } from '@/lib/copilot/generated/vfs-snapshot-v1' import { createBadRequestResponse, createUnauthorizedResponse } from '@/lib/copilot/request/http' import { createSSEStream, SSE_RESPONSE_HEADERS } from '@/lib/copilot/request/lifecycle/start' import { startCopilotOtelRoot, withCopilotSpan } from '@/lib/copilot/request/otel' @@ -184,6 +185,7 @@ type UnifiedChatBranch = prefetch?: boolean implicitFeedback?: string workspaceContext?: string + vfs?: VfsSnapshotV1 }) => Promise> buildExecutionContext: (params: { userId: string @@ -212,6 +214,7 @@ type UnifiedChatBranch = userTimezone?: string userMetadata?: { name?: string; email?: string; timezone?: string } workspaceContext?: string + vfs?: VfsSnapshotV1 }) => Promise> buildExecutionContext: (params: { userId: string @@ -618,6 +621,7 @@ async function resolveBranch(params: { prefetch: payloadParams.prefetch, implicitFeedback: payloadParams.implicitFeedback, workspaceContext: payloadParams.workspaceContext, + vfs: payloadParams.vfs, userPermission: payloadParams.userPermission, userTimezone: payloadParams.userTimezone, userMetadata: payloadParams.userMetadata, @@ -672,6 +676,7 @@ async function resolveBranch(params: { fileAttachments: payloadParams.fileAttachments, chatId: payloadParams.chatId, workspaceContext: payloadParams.workspaceContext, + vfs: payloadParams.vfs, userPermission: payloadParams.userPermission, userTimezone: payloadParams.userTimezone, userMetadata: payloadParams.userMetadata, @@ -902,7 +907,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { ? withCopilotSpan( TraceSpan.CopilotChatBuildWorkspaceContext, { [TraceAttr.WorkspaceId]: workspaceId }, - () => generateWorkspaceContext(workspaceId, authenticatedUserId), + () => generateWorkspaceSnapshot(workspaceId, authenticatedUserId), activeOtelRoot.context ) : Promise.resolve(undefined) @@ -947,7 +952,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { activeOtelRoot.context ) - const [agentContexts, userPermission, workspaceContext, , executionContext] = + const [agentContexts, userPermission, workspaceSnapshot, , executionContext] = await Promise.all([ agentContextsPromise, userPermissionPromise, @@ -955,6 +960,11 @@ export async function handleUnifiedChatPost(req: NextRequest) { persistUserMessagePromise, executionContextPromise, ]) + // Both halves come from one primary-db fetch (workspace-context.ts): + // `workspaceContext` is the markdown transition fallback, `vfs` is the + // typed snapshot Go diffs into baseline+delta messages. + const workspaceContext = workspaceSnapshot?.markdown + const vfs = workspaceSnapshot?.snapshot executionContext.userPermission = userPermission ?? undefined @@ -991,6 +1001,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { prefetch: body.prefetch, implicitFeedback: body.implicitFeedback, workspaceContext, + vfs, }) : branch.buildPayload({ message: body.message, @@ -1003,6 +1014,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { userTimezone: body.userTimezone, userMetadata, workspaceContext, + vfs, }), activeOtelRoot.context ) diff --git a/apps/sim/lib/copilot/chat/workspace-context.test.ts b/apps/sim/lib/copilot/chat/workspace-context.test.ts index 0c7850da2d3..bcb5398b63b 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.test.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.test.ts @@ -116,3 +116,157 @@ describe('buildWorkspaceMd - connected integrations / credentials', () => { expect(md).toContain('## Connected Integrations\n(none)') }) }) + +describe('buildWorkspaceMd - determinism (prompt-cache stability)', () => { + it('is byte-identical regardless of input row order', () => { + const a = buildWorkspaceMd( + baseData({ + members: [ + { name: 'Bob', email: 'bob@x.com', permissionType: 'admin' }, + { name: 'Amy', email: 'amy@x.com', permissionType: 'write' }, + ], + workflows: [ + { id: 'wf-2', name: 'Zeta', isDeployed: false, folderPath: null }, + { id: 'wf-1', name: 'Alpha', isDeployed: true, folderPath: null }, + ], + tables: [ + { id: 't-2', name: 'Orders', description: null, rowCount: 5 }, + { id: 't-1', name: 'Customers', description: null, rowCount: 9 }, + ], + knowledgeBases: [ + { id: 'kb-2', name: 'Docs', connectorTypes: ['notion', 'github'] }, + { id: 'kb-1', name: 'Articles', connectorTypes: ['github', 'notion'] }, + ], + oauthIntegrations: [ + { id: 'c-2', providerId: 'slack', displayName: null, role: null }, + { id: 'c-1', providerId: 'github', displayName: null, role: null }, + ], + envVariables: ['ZED', 'API_KEY'], + customTools: [ + { id: 'ct-2', name: 'Beta Tool' }, + { id: 'ct-1', name: 'Alpha Tool' }, + ], + mcpServers: [ + { id: 'mcp-2', name: 'Zulu', url: null, enabled: false }, + { id: 'mcp-1', name: 'Mike', url: 'https://x', enabled: true }, + ], + skills: [ + { id: 'sk-2', name: 'Writer', description: 'writes' }, + { id: 'sk-1', name: 'Editor', description: 'edits' }, + ], + jobs: [ + { + id: 'j-2', + title: 'Nightly', + prompt: 'run nightly', + cronExpression: '0 0 * * *', + status: 'active', + lifecycle: 'persistent', + sourceTaskName: null, + }, + { + id: 'j-1', + title: 'Hourly', + prompt: 'run hourly', + cronExpression: '0 * * * *', + status: 'active', + lifecycle: 'persistent', + sourceTaskName: null, + }, + ], + }) + ) + const b = buildWorkspaceMd( + baseData({ + members: [ + { name: 'Amy', email: 'amy@x.com', permissionType: 'write' }, + { name: 'Bob', email: 'bob@x.com', permissionType: 'admin' }, + ], + workflows: [ + { id: 'wf-1', name: 'Alpha', isDeployed: true, folderPath: null }, + { id: 'wf-2', name: 'Zeta', isDeployed: false, folderPath: null }, + ], + tables: [ + { id: 't-1', name: 'Customers', description: null, rowCount: 9 }, + { id: 't-2', name: 'Orders', description: null, rowCount: 5 }, + ], + knowledgeBases: [ + { id: 'kb-1', name: 'Articles', connectorTypes: ['notion', 'github'] }, + { id: 'kb-2', name: 'Docs', connectorTypes: ['github', 'notion'] }, + ], + oauthIntegrations: [ + { id: 'c-1', providerId: 'github', displayName: null, role: null }, + { id: 'c-2', providerId: 'slack', displayName: null, role: null }, + ], + envVariables: ['API_KEY', 'ZED'], + customTools: [ + { id: 'ct-1', name: 'Alpha Tool' }, + { id: 'ct-2', name: 'Beta Tool' }, + ], + mcpServers: [ + { id: 'mcp-1', name: 'Mike', url: 'https://x', enabled: true }, + { id: 'mcp-2', name: 'Zulu', url: null, enabled: false }, + ], + skills: [ + { id: 'sk-1', name: 'Editor', description: 'edits' }, + { id: 'sk-2', name: 'Writer', description: 'writes' }, + ], + jobs: [ + { + id: 'j-1', + title: 'Hourly', + prompt: 'run hourly', + cronExpression: '0 * * * *', + status: 'active', + lifecycle: 'persistent', + sourceTaskName: null, + }, + { + id: 'j-2', + title: 'Nightly', + prompt: 'run nightly', + cronExpression: '0 0 * * *', + status: 'active', + lifecycle: 'persistent', + sourceTaskName: null, + }, + ], + }) + ) + expect(a).toBe(b) + }) + + it('ignores volatile workflow run timestamps', () => { + const withRun = buildWorkspaceMd( + baseData({ + workflows: [ + { + id: 'wf-1', + name: 'Alpha', + isDeployed: false, + folderPath: null, + lastRunAt: new Date('2026-06-18T12:00:00Z'), + }, + ], + }) + ) + const withoutRun = buildWorkspaceMd( + baseData({ + workflows: [{ id: 'wf-1', name: 'Alpha', isDeployed: false, folderPath: null }], + }) + ) + expect(withRun).toBe(withoutRun) + expect(withRun).not.toContain('last run') + }) + + it('ignores volatile table row counts', () => { + const a = buildWorkspaceMd( + baseData({ tables: [{ id: 't-1', name: 'Customers', description: null, rowCount: 1 }] }) + ) + const b = buildWorkspaceMd( + baseData({ tables: [{ id: 't-1', name: 'Customers', description: null, rowCount: 9999 }] }) + ) + expect(a).toBe(b) + expect(a).not.toContain('rows') + }) +}) diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index d1a4b4ded82..2bbf5311a89 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -1,17 +1,21 @@ -import { dbReplica } from '@sim/db' +import { db } from '@sim/db' import { knowledgeBase, knowledgeConnector, mcpServers, userTableDefinitions, - userTableRows, workflow, workflowFolder, workflowSchedule, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { and, count, eq, inArray, isNull } from 'drizzle-orm' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import type { + VfsSnapshotV1, + VfsSnapshotV1Job, + VfsSnapshotV1Workflow, +} from '@/lib/copilot/generated/vfs-snapshot-v1' import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' import { canonicalWorkflowVfsDir, canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment' @@ -57,7 +61,11 @@ export interface WorkspaceMdData { description?: string | null connectorTypes?: string[] }> - tables: Array<{ id: string; name: string; description?: string | null; rowCount: number }> + // rowCount is no longer rendered (it is volatile and would bust the cached + // prompt prefix); kept optional so callers that still have it cheaply (the VFS + // materializer via listTables) need not change, while generateWorkspaceContext + // skips the per-table COUNT query entirely. + tables: Array<{ id: string; name: string; description?: string | null; rowCount?: number }> files: Array<{ id: string; name: string; type: string; size: number; folderPath?: string | null }> oauthIntegrations: Array<{ id: string @@ -81,9 +89,30 @@ export interface WorkspaceMdData { }> } +/** + * Deterministic string ordering. The workspace inventory is placed in the + * prompt-cache prefix (mothership), so its bytes must be identical for identical + * workspace state regardless of DB row order — otherwise the cache silently + * busts every turn. `localeCompare` with a pinned locale gives stable, readable + * ordering across Sim instances (all run the same Node/ICU build). + */ +function stableCompare(a: string, b: string): number { + return a.localeCompare(b, 'en') +} + +/** Stable order by display name, tie-broken by id, for inventory listings. */ +function byNameThenId(a: { name: string; id: string }, b: { name: string; id: string }): number { + return stableCompare(a.name, b.name) || stableCompare(a.id, b.id) +} + /** * Pure formatting: build WORKSPACE.md content from pre-fetched data. * No DB access — callers are responsible for providing the data. + * + * Output is deterministic: every collection is sorted by a stable key and + * volatile fields (run timestamps, mutable row counts) are omitted, so the + * rendered inventory only changes when the workspace structurally changes. This + * is what lets the mothership cache it in the prompt prefix across turns. */ export function buildWorkspaceMd(data: WorkspaceMdData): string { const sections: string[] = [] @@ -95,10 +124,12 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.members.length > 0) { - const lines = data.members.map((m) => { - const display = m.name ? `${m.name} (${m.email})` : m.email - return `- ${display} — ${m.permissionType}` - }) + const lines = [...data.members] + .sort((a, b) => stableCompare(a.email, b.email)) + .map((m) => { + const display = m.name ? `${m.name} (${m.email})` : m.email + return `- ${display} — ${m.permissionType}` + }) sections.push(`## Members\n${lines.join('\n')}`) } @@ -122,10 +153,11 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { parts.push(`${indent} VFS dir: \`${workflowDir}\``) parts.push(`${indent} VFS state path: \`${workflowDir}/state.json\``) if (wf.description) parts.push(`${indent} ${wf.description}`) - const flags: string[] = [] - if (wf.isDeployed) flags.push('deployed') - if (wf.lastRunAt) flags.push(`last run: ${wf.lastRunAt.toISOString().split('T')[0]}`) - if (flags.length > 0) parts[0] += ` — ${flags.join(', ')}` + // `deployed` is a structural flag (kept); `lastRunAt` is intentionally + // omitted — it changes on every run and would bust the cached prompt + // prefix that carries this inventory. Current run data lives in + // workflows/{name}/executions.json. + if (wf.isDeployed) parts[0] += ' — deployed' return parts.join('\n') } @@ -133,13 +165,13 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { lines.push( 'Use the canonical VFS dir/state path shown under each workflow. Paths are percent-encoded per segment; copy them verbatim and do not infer paths from display names.' ) - for (const wf of rootWorkflows) { + for (const wf of [...rootWorkflows].sort(byNameThenId)) { lines.push(formatWf(wf, '')) } - const sortedFolders = [...folderWorkflows.entries()].sort((a, b) => a[0].localeCompare(b[0])) + const sortedFolders = [...folderWorkflows.entries()].sort((a, b) => stableCompare(a[0], b[0])) for (const [folder, wfs] of sortedFolders) { lines.push(`- 📁 **${folder}/**`) - for (const wf of wfs) { + for (const wf of [...wfs].sort(byNameThenId)) { lines.push(formatWf(wf, ' ')) } } @@ -149,11 +181,11 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.knowledgeBases.length > 0) { - const lines = data.knowledgeBases.map((kb) => { + const lines = [...data.knowledgeBases].sort(byNameThenId).map((kb) => { let line = `- **${kb.name}** (${kb.id})` if (kb.description) line += ` — ${kb.description}` if (kb.connectorTypes && kb.connectorTypes.length > 0) { - line += ` | connectors: ${kb.connectorTypes.join(', ')}` + line += ` | connectors: ${[...kb.connectorTypes].sort(stableCompare).join(', ')}` } return line }) @@ -163,9 +195,11 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.tables.length > 0) { - const lines = data.tables.map((t) => { - let line = `- **${t.name}** (${t.id}) — ${t.rowCount} rows` - if (t.description) line += `, ${t.description}` + // rowCount is omitted: it changes on every row write and would bust the + // cached prompt prefix. Live counts are in tables/{name}/meta.json. + const lines = [...data.tables].sort(byNameThenId).map((t) => { + let line = `- **${t.name}** (${t.id})` + if (t.description) line += ` — ${t.description}` return line }) sections.push(`## Tables (${data.tables.length})\n${lines.join('\n')}`) @@ -192,13 +226,13 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { const lines: string[] = [ 'Read or edit a file by the exact VFS path shown in backticks below — copy it verbatim (it is already percent-encoded) and append `/content` to read the contents. Do not retype the display name or re-encode the path.', ] - for (const f of rootFiles) { + for (const f of [...rootFiles].sort(byNameThenId)) { lines.push(fileLine(f, '')) } - const sortedFolders = [...folderFiles.entries()].sort((a, b) => a[0].localeCompare(b[0])) + const sortedFolders = [...folderFiles.entries()].sort((a, b) => stableCompare(a[0], b[0])) for (const [folder, folderFileList] of sortedFolders) { lines.push(`- 📁 **${folder}/**`) - for (const f of folderFileList) { + for (const f of [...folderFileList].sort(byNameThenId)) { lines.push(fileLine(f, ' ')) } } @@ -208,13 +242,15 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.oauthIntegrations.length > 0) { - const lines = data.oauthIntegrations.map((c) => { - const services = PROVIDER_SERVICES[c.providerId] - const svc = services ? ` (${services.join(', ')})` : '' - const who = c.displayName ? ` — ${c.displayName}` : '' - const role = c.role ? `, ${c.role}` : '' - return `- ${c.providerId}${svc}${who}${role} — credentialId: \`${c.id}\`` - }) + const lines = [...data.oauthIntegrations] + .sort((a, b) => stableCompare(a.providerId, b.providerId) || stableCompare(a.id, b.id)) + .map((c) => { + const services = PROVIDER_SERVICES[c.providerId] + const svc = services ? ` (${services.join(', ')})` : '' + const who = c.displayName ? ` — ${c.displayName}` : '' + const role = c.role ? `, ${c.role}` : '' + return `- ${c.providerId}${svc}${who}${role} — credentialId: \`${c.id}\`` + }) sections.push( `## Connected Integrations\nPass these credentialId values directly on OAuth tool calls — no need to read environment/credentials.json for them.\n${lines.join('\n')}` ) @@ -223,17 +259,17 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.envVariables.length > 0) { - const lines = data.envVariables.map((v) => `- ${v}`) + const lines = [...data.envVariables].sort(stableCompare).map((v) => `- ${v}`) sections.push(`## Environment Variables (${data.envVariables.length})\n${lines.join('\n')}`) } if (data.customTools && data.customTools.length > 0) { - const lines = data.customTools.map((t) => `- **${t.name}** (${t.id})`) + const lines = [...data.customTools].sort(byNameThenId).map((t) => `- **${t.name}** (${t.id})`) sections.push(`## Custom Tools (${data.customTools.length})\n${lines.join('\n')}`) } if (data.mcpServers && data.mcpServers.length > 0) { - const lines = data.mcpServers.map((s) => { + const lines = [...data.mcpServers].sort(byNameThenId).map((s) => { const status = s.enabled ? 'enabled' : 'disabled' return `- **${s.name}** (${s.id}) — ${status}${s.url ? `, ${s.url}` : ''}` }) @@ -241,7 +277,9 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.skills && data.skills.length > 0) { - const lines = data.skills.map((s) => `- **${s.name}** (${s.id}) — ${s.description}`) + const lines = [...data.skills] + .sort(byNameThenId) + .map((s) => `- **${s.name}** (${s.id}) — ${s.description}`) sections.push( `## Skills (${data.skills.length})\n` + 'To use a skill, call the load_user_skill tool with its name to load the full instructions, then follow them. The descriptions below only say when each skill applies — they are not the instructions.\n' + @@ -250,16 +288,18 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.jobs && data.jobs.length > 0) { - const lines = data.jobs.map((j) => { - const displayName = j.title || j.id - let line = `- **${displayName}** (${j.id}) — ${j.status}` - if (j.lifecycle !== 'persistent') line += ` [${j.lifecycle}]` - if (j.cronExpression) line += `, cron: ${j.cronExpression}` - if (j.sourceTaskName) line += `, task: ${j.sourceTaskName}` - const promptPreview = j.prompt.length > 80 ? `${j.prompt.slice(0, 77)}...` : j.prompt - line += `\n ${promptPreview}` - return line - }) + const lines = [...data.jobs] + .sort((a, b) => stableCompare(a.title || a.id, b.title || b.id) || stableCompare(a.id, b.id)) + .map((j) => { + const displayName = j.title || j.id + let line = `- **${displayName}** (${j.id}) — ${j.status}` + if (j.lifecycle !== 'persistent') line += ` [${j.lifecycle}]` + if (j.cronExpression) line += `, cron: ${j.cronExpression}` + if (j.sourceTaskName) line += `, task: ${j.sourceTaskName}` + const promptPreview = j.prompt.length > 80 ? `${j.prompt.slice(0, 77)}...` : j.prompt + line += `\n ${promptPreview}` + return line + }) sections.push(`## Jobs (${data.jobs.length})\n${lines.join('\n')}`) } @@ -276,15 +316,20 @@ export function buildWorkspaceContextMd(data: WorkspaceMdData): string { * discovery rules; the LLM reads dynamic workspace state from VFS files. * The LLM never writes this file directly. */ -export async function generateWorkspaceContext( +// Fetch + assemble the workspace inventory data once, from the PRIMARY db +// (read-your-writes: a just-edited workflow is visible immediately, so the +// injected snapshot can't lag behind a `glob`). Both the markdown inventory and +// the typed VFS snapshot are built from this single fetch. Returns null when the +// workspace is unavailable or a fetch fails. +async function buildWorkspaceMdData( workspaceId: string, userId: string -): Promise { +): Promise { try { await assertActiveWorkspaceAccess(workspaceId, userId) const wsRow = await getWorkspaceWithOwner(workspaceId) if (!wsRow) { - return '## Workspace\n(unavailable)' + return null } const [ @@ -302,7 +347,7 @@ export async function generateWorkspaceContext( ] = await Promise.all([ getUsersWithPermissions(workspaceId), - dbReplica + db .select({ id: workflow.id, name: workflow.name, @@ -314,7 +359,7 @@ export async function generateWorkspaceContext( .from(workflow) .where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt))), - dbReplica + db .select({ id: workflowFolder.id, name: workflowFolder.name, @@ -323,7 +368,7 @@ export async function generateWorkspaceContext( .from(workflowFolder) .where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt))), - dbReplica + db .select({ id: knowledgeBase.id, name: knowledgeBase.name, @@ -332,7 +377,7 @@ export async function generateWorkspaceContext( .from(knowledgeBase) .where(and(eq(knowledgeBase.workspaceId, workspaceId), isNull(knowledgeBase.deletedAt))), - dbReplica + db .select({ id: userTableDefinitions.id, name: userTableDefinitions.name, @@ -352,7 +397,7 @@ export async function generateWorkspaceContext( listCustomTools({ userId, workspaceId }), - dbReplica + db .select({ id: mcpServers.id, name: mcpServers.name, @@ -364,7 +409,7 @@ export async function generateWorkspaceContext( listSkills({ workspaceId, includeBuiltins: false }), - dbReplica + db .select({ id: workflowSchedule.id, jobTitle: workflowSchedule.jobTitle, @@ -384,23 +429,10 @@ export async function generateWorkspaceContext( ), ]) - const rowCounts = - tables.length > 0 - ? await Promise.all( - tables.map(async (t) => { - const [row] = await dbReplica - .select({ count: count() }) - .from(userTableRows) - .where(eq(userTableRows.tableId, t.id)) - return row?.count ?? 0 - }) - ) - : [] - const kbIds = kbs.map((kb) => kb.id) const connectorRows = kbIds.length > 0 - ? await dbReplica + ? await db .select({ knowledgeBaseId: knowledgeConnector.knowledgeBaseId, connectorType: knowledgeConnector.connectorType, @@ -437,7 +469,7 @@ export async function generateWorkspaceContext( return path } - return buildWorkspaceMd({ + return { workspace: wsRow, members, workflows: workflows.map((wf) => ({ @@ -446,9 +478,13 @@ export async function generateWorkspaceContext( })), knowledgeBases: kbs.map((kb) => ({ ...kb, - connectorTypes: connectorTypesByKb.get(kb.id), + // Sort connector types so the snapshot is order-stable: the DB query has + // no ORDER BY, and the Go delta engine compares item JSON byte-wise, so + // an unsorted (but unchanged) list would emit a spurious "modified" + // delta and needlessly bust the prompt cache. + connectorTypes: connectorTypesByKb.get(kb.id)?.sort(stableCompare), })), - tables: tables.map((t, i) => ({ ...t, rowCount: rowCounts[i] ?? 0 })), + tables: tables.map((t) => ({ id: t.id, name: t.name, description: t.description })), files: files.map((f) => ({ id: f.id, name: f.name, @@ -477,13 +513,128 @@ export async function generateWorkspaceContext( lifecycle: j.lifecycle, sourceTaskName: j.sourceTaskName, })), - }) + } } catch (err) { - logger.error('Failed to generate workspace context', { + logger.error('Failed to build workspace data', { workspaceId, error: toError(err).message, }) - return '## Workspace\n(unavailable)\n\n## Workflows\n(unavailable)\n\n## Knowledge Bases\n(unavailable)\n\n## Tables\n(unavailable)\n\n## Files\n(unavailable)\n\n## Connected Integrations\n(unavailable)' + return null + } +} + +const WORKSPACE_CONTEXT_UNAVAILABLE_MD = + '## Workspace\n(unavailable)\n\n## Workflows\n(unavailable)\n\n## Knowledge Bases\n(unavailable)\n\n## Tables\n(unavailable)\n\n## Files\n(unavailable)\n\n## Connected Integrations\n(unavailable)' + +/** + * Generate WORKSPACE.md markdown from current DB state (primary db). The LLM + * reads dynamic workspace state from VFS files; it never writes this file. + */ +export async function generateWorkspaceContext( + workspaceId: string, + userId: string +): Promise { + const data = await buildWorkspaceMdData(workspaceId, userId) + return data ? buildWorkspaceMd(data) : WORKSPACE_CONTEXT_UNAVAILABLE_MD +} + +/** + * Build BOTH the markdown inventory and the typed VFS snapshot from a single + * primary-db fetch. The snapshot is the structured form Go diffs into + * baseline+delta messages; the markdown is the transition fallback. Returns null + * when the workspace is unavailable. + */ +export async function generateWorkspaceSnapshot( + workspaceId: string, + userId: string +): Promise<{ markdown: string; snapshot: VfsSnapshotV1 } | null> { + const data = await buildWorkspaceMdData(workspaceId, userId) + if (!data) return null + return { markdown: buildWorkspaceMd(data), snapshot: buildVfsSnapshot(data) } +} + +/** + * Map the workspace inventory data to the typed VFS snapshot contract. Pure; + * mirrors buildWorkspaceMd's field selection. Resource order is irrelevant — Go + * diffs by stable id, not position. + */ +export function buildVfsSnapshot(data: WorkspaceMdData): VfsSnapshotV1 { + const workflows: VfsSnapshotV1Workflow[] = data.workflows.map((wf) => ({ + id: wf.id, + name: wf.name, + path: canonicalWorkflowVfsDir({ name: wf.name, folderPath: wf.folderPath }), + ...(wf.description ? { description: wf.description } : {}), + ...(wf.isDeployed ? { isDeployed: true } : {}), + ...(wf.folderPath ? { folderPath: wf.folderPath } : {}), + })) + const jobs: VfsSnapshotV1Job[] = (data.jobs ?? []) + .filter((j) => j.status !== 'completed') + .map((j) => ({ + id: j.id, + ...(j.title ? { title: j.title } : {}), + ...(j.prompt ? { prompt: j.prompt } : {}), + ...(j.cronExpression ? { cronExpression: j.cronExpression } : {}), + ...(j.status ? { status: j.status } : {}), + ...(j.lifecycle ? { lifecycle: j.lifecycle } : {}), + ...(j.sourceTaskName ? { sourceTaskName: j.sourceTaskName } : {}), + })) + return { + ...(data.workspace + ? { + workspace: { + id: data.workspace.id, + name: data.workspace.name, + ...(data.workspace.ownerId ? { ownerId: data.workspace.ownerId } : {}), + }, + } + : {}), + members: data.members.map((m) => ({ + ...(m.name ? { name: m.name } : {}), + email: m.email, + ...(m.permissionType ? { permissionType: m.permissionType } : {}), + })), + workflows, + knowledgeBases: data.knowledgeBases.map((kb) => ({ + id: kb.id, + name: kb.name, + ...(kb.description ? { description: kb.description } : {}), + ...(kb.connectorTypes && kb.connectorTypes.length > 0 + ? { connectorTypes: kb.connectorTypes } + : {}), + })), + tables: data.tables.map((t) => ({ + id: t.id, + name: t.name, + ...(t.description ? { description: t.description } : {}), + })), + files: data.files.map((f) => ({ + id: f.id, + name: f.name, + path: canonicalWorkspaceFilePath({ folderPath: f.folderPath, name: f.name }), + ...(f.type ? { type: f.type } : {}), + ...(f.size ? { size: f.size } : {}), + ...(f.folderPath ? { folderPath: f.folderPath } : {}), + })), + integrations: data.oauthIntegrations.map((c) => ({ + id: c.id, + providerId: c.providerId, + ...(c.displayName ? { displayName: c.displayName } : {}), + ...(c.role ? { role: c.role } : {}), + })), + envVars: data.envVariables, + customTools: (data.customTools ?? []).map((t) => ({ id: t.id, name: t.name })), + mcpServers: (data.mcpServers ?? []).map((s) => ({ + id: s.id, + name: s.name, + ...(s.url ? { url: s.url } : {}), + ...(s.enabled ? { enabled: true } : {}), + })), + skills: (data.skills ?? []).map((s) => ({ + id: s.id, + name: s.name, + ...(s.description ? { description: s.description } : {}), + })), + jobs, } } diff --git a/apps/sim/lib/copilot/generated/metrics-v1.ts b/apps/sim/lib/copilot/generated/metrics-v1.ts index dd8527f8158..fefc4534ea3 100644 --- a/apps/sim/lib/copilot/generated/metrics-v1.ts +++ b/apps/sim/lib/copilot/generated/metrics-v1.ts @@ -24,6 +24,7 @@ export const Metric = { CopilotRequestDuration: 'copilot.request.duration', CopilotToolCalls: 'copilot.tool.calls', CopilotToolDuration: 'copilot.tool.duration', + CopilotVfsDelta: 'copilot.vfs.delta', CopilotVfsMaterializeDuration: 'copilot.vfs.materialize.duration', GenAiClientCacheTokenUsage: 'gen_ai.client.cache.token.usage', GenAiClientTokenUsage: 'gen_ai.client.token.usage', @@ -48,6 +49,7 @@ export const MetricValues: readonly MetricValue[] = [ 'copilot.request.duration', 'copilot.tool.calls', 'copilot.tool.duration', + 'copilot.vfs.delta', 'copilot.vfs.materialize.duration', 'gen_ai.client.cache.token.usage', 'gen_ai.client.token.usage', diff --git a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts index 982857673f5..c5024ee3571 100644 --- a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts @@ -266,6 +266,7 @@ export const TraceAttr = { CopilotTransport: 'copilot.transport', CopilotUserMessagePreview: 'copilot.user.message_preview', CopilotValidateOutcome: 'copilot.validate.outcome', + CopilotVfsDeltaOutcome: 'copilot.vfs.delta.outcome', CopilotVfsFileExtension: 'copilot.vfs.file.extension', CopilotVfsFileMediaType: 'copilot.vfs.file.media_type', CopilotVfsFileName: 'copilot.vfs.file.name', @@ -411,6 +412,8 @@ export const TraceAttr = { GenAiStreamPhaseToolArgsFirstMs: 'gen_ai.stream.phase.tool_args.first_ms', GenAiStreamPhaseToolArgsMs: 'gen_ai.stream.phase.tool_args.ms', GenAiSystem: 'gen_ai.system', + GenAiThinkingBlocksCaptured: 'gen_ai.thinking.blocks_captured', + GenAiThinkingRedactedBlocks: 'gen_ai.thinking.redacted_blocks', GenAiTokenType: 'gen_ai.token.type', GenAiToolName: 'gen_ai.tool.name', GenAiUsageCacheCreationInputTokens: 'gen_ai.usage.cache_creation.input_tokens', @@ -890,6 +893,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'copilot.transport', 'copilot.user.message_preview', 'copilot.validate.outcome', + 'copilot.vfs.delta.outcome', 'copilot.vfs.file.extension', 'copilot.vfs.file.media_type', 'copilot.vfs.file.name', @@ -1024,6 +1028,8 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'gen_ai.stream.phase.tool_args.first_ms', 'gen_ai.stream.phase.tool_args.ms', 'gen_ai.system', + 'gen_ai.thinking.blocks_captured', + 'gen_ai.thinking.redacted_blocks', 'gen_ai.token.type', 'gen_ai.tool.name', 'gen_ai.usage.cache_creation.input_tokens', diff --git a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts new file mode 100644 index 00000000000..9a4df7519b1 --- /dev/null +++ b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts @@ -0,0 +1,131 @@ +// AUTO-GENERATED FILE. DO NOT EDIT. +// + +/** + * Structured workspace inventory snapshot Sim sends to Go; Go diffs successive snapshots into baseline+delta messages. + */ +export interface VfsSnapshotV1 { + customTools?: VfsSnapshotV1NamedResource[] + envVars?: string[] + files?: VfsSnapshotV1File[] + integrations?: VfsSnapshotV1Integration[] + jobs?: VfsSnapshotV1Job[] + knowledgeBases?: VfsSnapshotV1KnowledgeBase[] + mcpServers?: VfsSnapshotV1McpServer[] + members?: VfsSnapshotV1Member[] + skills?: VfsSnapshotV1Skill[] + tables?: VfsSnapshotV1Table[] + workflows?: VfsSnapshotV1Workflow[] + workspace?: VfsSnapshotV1Workspace +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1NamedResource". + */ +export interface VfsSnapshotV1NamedResource { + id: string + name: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1File". + */ +export interface VfsSnapshotV1File { + folderPath?: string + id: string + name: string + path: string + size?: number + type?: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Integration". + */ +export interface VfsSnapshotV1Integration { + displayName?: string + id: string + providerId: string + role?: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Job". + */ +export interface VfsSnapshotV1Job { + cronExpression?: string + id: string + lifecycle?: string + prompt?: string + sourceTaskName?: string + status?: string + title?: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1KnowledgeBase". + */ +export interface VfsSnapshotV1KnowledgeBase { + connectorTypes?: string[] + description?: string + id: string + name: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1McpServer". + */ +export interface VfsSnapshotV1McpServer { + enabled?: boolean + id: string + name: string + url?: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Member". + */ +export interface VfsSnapshotV1Member { + email: string + name?: string + permissionType?: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Skill". + */ +export interface VfsSnapshotV1Skill { + description?: string + id: string + name: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Table". + */ +export interface VfsSnapshotV1Table { + description?: string + id: string + name: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Workflow". + */ +export interface VfsSnapshotV1Workflow { + description?: string + folderPath?: string + id: string + isDeployed?: boolean + name: string + path: string +} +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1Workspace". + */ +export interface VfsSnapshotV1Workspace { + id: string + name: string + ownerId?: string +} diff --git a/apps/sim/lib/copilot/tools/handlers/vfs.ts b/apps/sim/lib/copilot/tools/handlers/vfs.ts index e41c2b34b82..ca1902692e1 100644 --- a/apps/sim/lib/copilot/tools/handlers/vfs.ts +++ b/apps/sim/lib/copilot/tools/handlers/vfs.ts @@ -122,7 +122,7 @@ export async function executeVfsGrep( const vfs = await getOrMaterializeVFS(workspaceId, context.userId) result = isWorkspaceFileGrepPath(rawPath) ? await vfs.grepFile(rawPath, pattern, grepOptions) - : vfs.grep(pattern, rawPath, grepOptions) + : await vfs.grep(pattern, rawPath, grepOptions) } const key = outputMode === 'files_with_matches' ? 'files' : outputMode === 'count' ? 'counts' : 'matches' @@ -324,7 +324,7 @@ export async function executeVfsRead( } } - const result = vfs.read(path, offset, limit) + const result = await vfs.read(path, offset, limit) if (!result) { const suggestions = vfs.suggestSimilar(path) logger.warn('vfs_read file not found', { path, suggestions }) diff --git a/apps/sim/lib/copilot/vfs/operations.ts b/apps/sim/lib/copilot/vfs/operations.ts index dfef44a1ae5..bd7208719b8 100644 --- a/apps/sim/lib/copilot/vfs/operations.ts +++ b/apps/sim/lib/copilot/vfs/operations.ts @@ -108,8 +108,11 @@ function splitLinesForGrep(content: string): string[] { * (including `[`, `{`, spaces) use directory-prefix logic so literal VFS path segments are not * parsed as glob syntax. Trailing slashes are stripped so `files/` and `files` both scope under * `files/...`. + * + * Exported so the lazy VFS can resolve exactly the lazy artifacts a scoped grep will consider, + * keeping "what we materialize" identical to "what grep filters in". */ -function pathWithinGrepScope(filePath: string, scope: string): boolean { +export function pathWithinGrepScope(filePath: string, scope: string): boolean { const scopeUsesStarOrQuestionGlob = /[*?]/.test(scope) if (scopeUsesStarOrQuestionGlob) { return micromatch.isMatch(filePath, scope, VFS_GLOB_OPTIONS) diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 7e51de7093c..41a30b1b2a7 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -379,7 +379,24 @@ function getStaticComponentFiles(): Map { * components/triggers/{provider}/{id}.json (external triggers: github, slack, etc.) */ export class WorkspaceVFS { + // Eagerly-materialized, cheap content (structure + metadata): folder markers, + // per-resource meta.json, WORKSPACE.md/WORKSPACE_CONTEXT.md, static components. private files: Map = new Map() + // Lazily-materialized, expensive content keyed by VFS path. The loader runs on + // demand: a `read` resolves exactly one entry; a scoped `grep` resolves only + // the entries within its scope; an unscoped `grep` resolves all; a `glob` never + // resolves any (it matches keys only). This is why a read/glob no longer pays + // for every workflow's graph-load + lint + stringify — only grep over contents + // does, and only for what it actually scans. + private lazy: Map Promise> = new Map() + // Per-instance (per-tool-call) memo so state.json + lint.json for the same + // workflow share one normalized-table load, and deployment.json + versions.json + // share one deployment query. + private normalizedCache = new Map< + string, + Promise>> + >() + private deploymentCache = new Map>() private _workspaceId = '' private _betaEnabled = false @@ -387,6 +404,108 @@ export class WorkspaceVFS { return this._workspaceId } + /** Register a VFS path whose (expensive) content is produced on demand. */ + private registerLazy(path: string, loader: () => Promise): void { + this.lazy.set(path, loader) + } + + /** + * Load a workflow's normalized state once per instance. state.json and lint.json + * both need it, and a grep over a workflow's dir touches both — without this they + * would each re-load the full block graph. + */ + private loadNormalized( + workflowId: string + ): Promise>> { + let cached = this.normalizedCache.get(workflowId) + if (!cached) { + cached = loadWorkflowFromNormalizedTables(workflowId) + this.normalizedCache.set(workflowId, cached) + } + return cached + } + + /** Load a workflow's deployment data once per instance (deployment.json + versions.json share it). */ + private loadDeployments(wf: { + id: string + isDeployed: boolean + deployedAt: Date | null + }): Promise { + let cached = this.deploymentCache.get(wf.id) + if (!cached) { + cached = this.getWorkflowDeployments(wf.id, this._workspaceId, wf.isDeployed, wf.deployedAt) + this.deploymentCache.set(wf.id, cached) + } + return cached + } + + /** + * Resolve a single lazy artifact into {@link files}. Idempotent: once resolved + * the entry moves to `files` and the loader is dropped. A loader that returns + * null (no data) leaves nothing behind, so the path reads as "not found". + */ + private async resolveLazyPath(path: string): Promise { + const existing = this.files.get(path) + if (existing !== undefined) return existing + const loader = this.lazy.get(path) + if (!loader) return null + this.lazy.delete(path) + let content: string | null = null + try { + content = await loader() + } catch (err) { + logger.warn('Failed to resolve lazy VFS artifact', { + workspaceId: this._workspaceId, + path, + error: toError(err).message, + }) + content = null + } + if (content !== null) this.files.set(path, content) + return content + } + + /** + * Resolve every lazy artifact a grep over `scope` will scan, in parallel. An + * undefined scope (unscoped grep) resolves all — the worst case, equivalent to + * the old eager full materialize, but now only paid by an unscoped grep. + * Uses the same scope matcher as {@link ops.grep} so the materialized set is + * exactly the set grep filters in. + */ + private async resolveLazyWithinScope(scope?: string): Promise { + const targets: string[] = [] + for (const path of this.lazy.keys()) { + if (!scope || ops.pathWithinGrepScope(path, scope)) targets.push(path) + } + if (targets.length === 0) return + await Promise.all(targets.map((path) => this.resolveLazyPath(path))) + } + + /** + * `recently-deleted/` artifacts are opt-in: excluded from the active view + * unless a path/pattern explicitly scopes into them. + */ + private isRecentlyDeleted(key: string): boolean { + return key.startsWith('recently-deleted/') + } + + /** + * A keys-only view (eager values plus empty placeholders for unresolved lazy + * paths) for glob/suggestSimilar, which match on keys and never read content. + */ + private keyView(includeDeleted: boolean): Map { + const view = new Map() + for (const [key, value] of this.files) { + if (includeDeleted || !this.isRecentlyDeleted(key)) view.set(key, value) + } + for (const key of this.lazy.keys()) { + if ((includeDeleted || !this.isRecentlyDeleted(key)) && !view.has(key)) { + view.set(key, '') + } + } + return view + } + /** * Materialize workspace data into the VFS. * Uses shared service functions for all data access, then generates @@ -395,6 +514,9 @@ export class WorkspaceVFS { async materialize(workspaceId: string, userId: string): Promise { const start = Date.now() this.files = new Map() + this.lazy = new Map() + this.normalizedCache = new Map() + this.deploymentCache = new Map() this._workspaceId = workspaceId this._betaEnabled = await isFeatureEnabled('mothership-beta', { userId }) @@ -508,7 +630,7 @@ export class WorkspaceVFS { private activeFiles(): Map { const filtered = new Map() for (const [key, value] of this.files) { - if (!key.startsWith('recently-deleted/')) { + if (!this.isRecentlyDeleted(key)) { filtered.set(key, value) } } @@ -520,11 +642,14 @@ export class WorkspaceVFS { return this.activeFiles() } - grep( + async grep( pattern: string, path?: string, options?: GrepOptions - ): GrepMatch[] | string[] | ops.GrepCountEntry[] { + ): Promise { + // grep is the only op that scans contents, so it is the only op that pays to + // materialize lazy artifacts — and only those within its scope. + await this.resolveLazyWithinScope(path) return ops.grep(this.filesForPath(path), pattern, path, options) } @@ -579,16 +704,23 @@ export class WorkspaceVFS { } glob(pattern: string): string[] { - const target = pattern.startsWith('recently-deleted') ? this.files : this.activeFiles() - return ops.glob(target, pattern) + // glob matches keys only, so it resolves no lazy content — it sees the full + // path structure (eager keys + lazy placeholders) for free. + const includeDeleted = pattern.startsWith('recently-deleted') + return ops.glob(this.keyView(includeDeleted), pattern) } - read(path: string, offset?: number, limit?: number): ReadResult | null { + async read(path: string, offset?: number, limit?: number): Promise { + // Resolve the one lazy artifact being read into `files`; a no-op for eager + // paths (already present) and unknown paths (no loader). Lazy keys are always + // ASCII (built via encodeURIComponent), so no Unicode-normalized lookup is + // needed here; ops.read still does its own NFC/NFD fallback over `files`. + await this.resolveLazyPath(path) return ops.read(this.files, path, offset, limit) } suggestSimilar(missingPath: string, max?: number): string[] { - return ops.suggestSimilar(this.files, missingPath, max) + return ops.suggestSimilar(this.keyView(true), missingPath, max) } private async resolveWorkspaceFileForDynamicRead( @@ -1160,118 +1292,87 @@ export class WorkspaceVFS { ) } - let normalized: Awaited> = null - try { - normalized = await loadWorkflowFromNormalizedTables(wf.id) - if (normalized) { - const sanitized = sanitizeForCopilot({ - blocks: normalized.blocks, - edges: normalized.edges, - loops: normalized.loops, - parallels: normalized.parallels, - } as any) - this.files.set(`${prefix}state.json`, JSON.stringify(sanitized, null, 2)) - - // Dynamically-computed validation state (lint.json), derived from - // the raw normalized state so subBlock values, advancedMode, - // canonicalModes, and subflow edges are all available. - // - // CPU-only by design: tier-2 reference resolution - // (collectUnresolvedReferences) runs DB queries per selector field - // and is validated where it matters — at edit_workflow apply time. - // Running it here meant workflows × selectors sequential DB queries - // on every read/glob/grep call, which is what made `files/` reads - // take ~40s in large workspaces. - try { - const graphLint = lintEditedWorkflowState(normalized as any) - const fieldIssues = collectWorkflowFieldIssues(normalized.blocks as any) - this.files.set( - `${prefix}lint.json`, - JSON.stringify( - { - ...graphLint, - fieldIssues, - notes: [ - UNRESOLVABLE_AT_LINT_NOTE, - 'Credential/resource reference resolution is validated when editing the workflow, not in this snapshot.', - ], - }, - null, - 2 - ) - ) - } catch (lintErr) { - logger.warn('Failed to compute lint.json', { - workflowId: wf.id, - error: toError(lintErr).message, - }) - } - } else { - // loadWorkflowFromNormalizedTables returns null when the workflow has - // zero block rows. A block-less workflow still exists and must be - // readable, so emit an empty-but-valid state.json — otherwise - // read("workflows/{path}/state.json") 404s and suggestSimilar points the - // agent at a different, same-named workflow. dag/lint are derived from - // blocks and are omitted for the empty case. - this.files.set( - `${prefix}state.json`, - JSON.stringify( - sanitizeForCopilot({ blocks: {}, edges: [], loops: {}, parallels: {} } as any), - null, - 2 - ) - ) - } - } catch (err) { - logger.warn('Failed to load workflow state', { - workflowId: wf.id, - error: toError(err).message, - }) - } + // Heavy per-workflow content is LAZY: a read/glob never loads the block + // graph, runs lint, or queries executions/deployments. Only a read of the + // specific artifact — or a grep whose scope touches it — resolves it. + // state.json + lint.json share one memoized normalized-table load; + // deployment.json + versions.json share one memoized deployment query. + // This is the change that stops every read/glob from paying O(workflows) + // graph-loads + lint + stringify (what made large-workspace reads ~40s). + this.registerLazy(`${prefix}state.json`, async () => { + const normalized = await this.loadNormalized(wf.id) + // loadWorkflowFromNormalizedTables returns null for a zero-block + // workflow; it still exists and must be readable, so emit an + // empty-but-valid state.json rather than a 404. + const sanitized = normalized + ? sanitizeForCopilot({ + blocks: normalized.blocks, + edges: normalized.edges, + loops: normalized.loops, + parallels: normalized.parallels, + } as any) + : sanitizeForCopilot({ blocks: {}, edges: [], loops: {}, parallels: {} } as any) + return JSON.stringify(sanitized, null, 2) + }) - try { - const execRows = await db - .select({ - id: workflowExecutionLogs.id, - executionId: workflowExecutionLogs.executionId, - status: workflowExecutionLogs.status, - trigger: workflowExecutionLogs.trigger, - startedAt: workflowExecutionLogs.startedAt, - endedAt: workflowExecutionLogs.endedAt, - totalDurationMs: workflowExecutionLogs.totalDurationMs, - }) - .from(workflowExecutionLogs) - .where(eq(workflowExecutionLogs.workflowId, wf.id)) - .orderBy(desc(workflowExecutionLogs.startedAt)) - .limit(5) + this.registerLazy(`${prefix}lint.json`, async () => { + const normalized = await this.loadNormalized(wf.id) + // Derived from the raw normalized state (subBlock values, advancedMode, + // canonicalModes, subflow edges). CPU-only by design: tier-2 reference + // resolution runs at edit_workflow apply time, not here. A zero-block + // workflow has no lint (reads as not-found, as before). + if (!normalized) return null + const graphLint = lintEditedWorkflowState(normalized as any) + const fieldIssues = collectWorkflowFieldIssues(normalized.blocks as any) + return JSON.stringify( + { + ...graphLint, + fieldIssues, + notes: [ + UNRESOLVABLE_AT_LINT_NOTE, + 'Credential/resource reference resolution is validated when editing the workflow, not in this snapshot.', + ], + }, + null, + 2 + ) + }) - if (execRows.length > 0) { - this.files.set(`${prefix}executions.json`, serializeRecentExecutions(execRows)) - } - } catch (err) { - logger.warn('Failed to load execution logs', { - workflowId: wf.id, - error: toError(err).message, + // executions.json is advertised only when the workflow has run (cheap + // signal: lastRunAt), matching the old "set iff execRows > 0" behavior + // without the per-workflow query on every tool call. + if (wf.lastRunAt) { + this.registerLazy(`${prefix}executions.json`, async () => { + const execRows = await db + .select({ + id: workflowExecutionLogs.id, + executionId: workflowExecutionLogs.executionId, + status: workflowExecutionLogs.status, + trigger: workflowExecutionLogs.trigger, + startedAt: workflowExecutionLogs.startedAt, + endedAt: workflowExecutionLogs.endedAt, + totalDurationMs: workflowExecutionLogs.totalDurationMs, + }) + .from(workflowExecutionLogs) + .where(eq(workflowExecutionLogs.workflowId, wf.id)) + .orderBy(desc(workflowExecutionLogs.startedAt)) + .limit(5) + return execRows.length > 0 ? serializeRecentExecutions(execRows) : null }) } - try { - const deploymentData = await this.getWorkflowDeployments( - wf.id, - workspaceId, - wf.isDeployed, - wf.deployedAt - ) - if (deploymentData) { - this.files.set(`${prefix}deployment.json`, serializeDeployments(deploymentData)) - if (deploymentData.versions && deploymentData.versions.length > 0) { - this.files.set(`${prefix}versions.json`, serializeVersions(deploymentData.versions)) - } - } - } catch (err) { - logger.warn('Failed to load deployment data', { - workflowId: wf.id, - error: toError(err).message, + // deployment.json / versions.json are advertised when the workflow is + // deployed (cheap signal: isDeployed). Both share one memoized query. + if (wf.isDeployed) { + this.registerLazy(`${prefix}deployment.json`, async () => { + const deploymentData = await this.loadDeployments(wf) + return deploymentData ? serializeDeployments(deploymentData) : null + }) + this.registerLazy(`${prefix}versions.json`, async () => { + const deploymentData = await this.loadDeployments(wf) + return deploymentData?.versions && deploymentData.versions.length > 0 + ? serializeVersions(deploymentData.versions) + : null }) } }) @@ -1318,70 +1419,61 @@ export class WorkspaceVFS { }) ) - try { - const docRows = await db - .select({ - id: document.id, - filename: document.filename, - fileSize: document.fileSize, - mimeType: document.mimeType, - chunkCount: document.chunkCount, - tokenCount: document.tokenCount, - processingStatus: document.processingStatus, - enabled: document.enabled, - uploadedAt: document.uploadedAt, - }) - .from(document) - .where( - and( - eq(document.knowledgeBaseId, kb.id), - eq(document.userExcluded, false), - isNull(document.archivedAt), - isNull(document.deletedAt) + // documents.json / connectors.json are lazy, advertised only when the KB + // summary says they exist (docCount / connectorTypes) — no per-KB query on + // a read/glob, only when the artifact is read or grepped. + if (kb.docCount > 0) { + this.registerLazy(`${prefix}documents.json`, async () => { + const docRows = await db + .select({ + id: document.id, + filename: document.filename, + fileSize: document.fileSize, + mimeType: document.mimeType, + chunkCount: document.chunkCount, + tokenCount: document.tokenCount, + processingStatus: document.processingStatus, + enabled: document.enabled, + uploadedAt: document.uploadedAt, + }) + .from(document) + .where( + and( + eq(document.knowledgeBaseId, kb.id), + eq(document.userExcluded, false), + isNull(document.archivedAt), + isNull(document.deletedAt) + ) ) - ) - - if (docRows.length > 0) { - this.files.set(`${prefix}documents.json`, serializeDocuments(docRows)) - } - } catch (err) { - logger.warn('Failed to load KB documents', { - knowledgeBaseId: kb.id, - error: toError(err).message, + return docRows.length > 0 ? serializeDocuments(docRows) : null }) } - try { - const connectorRows = await db - .select({ - id: knowledgeConnector.id, - connectorType: knowledgeConnector.connectorType, - status: knowledgeConnector.status, - syncMode: knowledgeConnector.syncMode, - syncIntervalMinutes: knowledgeConnector.syncIntervalMinutes, - lastSyncAt: knowledgeConnector.lastSyncAt, - lastSyncError: knowledgeConnector.lastSyncError, - lastSyncDocCount: knowledgeConnector.lastSyncDocCount, - nextSyncAt: knowledgeConnector.nextSyncAt, - consecutiveFailures: knowledgeConnector.consecutiveFailures, - createdAt: knowledgeConnector.createdAt, - }) - .from(knowledgeConnector) - .where( - and( - eq(knowledgeConnector.knowledgeBaseId, kb.id), - isNull(knowledgeConnector.archivedAt), - isNull(knowledgeConnector.deletedAt) + if (kb.connectorTypes.length > 0) { + this.registerLazy(`${prefix}connectors.json`, async () => { + const connectorRows = await db + .select({ + id: knowledgeConnector.id, + connectorType: knowledgeConnector.connectorType, + status: knowledgeConnector.status, + syncMode: knowledgeConnector.syncMode, + syncIntervalMinutes: knowledgeConnector.syncIntervalMinutes, + lastSyncAt: knowledgeConnector.lastSyncAt, + lastSyncError: knowledgeConnector.lastSyncError, + lastSyncDocCount: knowledgeConnector.lastSyncDocCount, + nextSyncAt: knowledgeConnector.nextSyncAt, + consecutiveFailures: knowledgeConnector.consecutiveFailures, + createdAt: knowledgeConnector.createdAt, + }) + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.knowledgeBaseId, kb.id), + isNull(knowledgeConnector.archivedAt), + isNull(knowledgeConnector.deletedAt) + ) ) - ) - - if (connectorRows.length > 0) { - this.files.set(`${prefix}connectors.json`, serializeConnectors(connectorRows)) - } - } catch (err) { - logger.warn('Failed to load KB connectors', { - knowledgeBaseId: kb.id, - error: toError(err).message, + return connectorRows.length > 0 ? serializeConnectors(connectorRows) : null }) } }) @@ -1956,29 +2048,25 @@ export class WorkspaceVFS { this.files.set(`jobs/${safeName}/history.json`, JSON.stringify(history, null, 2)) } - try { - const execRows = await db - .select({ - id: jobExecutionLogs.id, - executionId: jobExecutionLogs.executionId, - status: jobExecutionLogs.status, - trigger: jobExecutionLogs.trigger, - startedAt: jobExecutionLogs.startedAt, - endedAt: jobExecutionLogs.endedAt, - totalDurationMs: jobExecutionLogs.totalDurationMs, - }) - .from(jobExecutionLogs) - .where(eq(jobExecutionLogs.scheduleId, job.id)) - .orderBy(desc(jobExecutionLogs.startedAt)) - .limit(5) - - if (execRows.length > 0) { - this.files.set(`jobs/${safeName}/executions.json`, serializeRecentExecutions(execRows)) - } - } catch (err) { - logger.warn('Failed to load job execution logs', { - jobId: job.id, - error: toError(err).message, + // executions.json is lazy, advertised only when the job has run (cheap + // signal: lastRanAt) — no per-job query on a read/glob. + if (job.lastRanAt) { + this.registerLazy(`jobs/${safeName}/executions.json`, async () => { + const execRows = await db + .select({ + id: jobExecutionLogs.id, + executionId: jobExecutionLogs.executionId, + status: jobExecutionLogs.status, + trigger: jobExecutionLogs.trigger, + startedAt: jobExecutionLogs.startedAt, + endedAt: jobExecutionLogs.endedAt, + totalDurationMs: jobExecutionLogs.totalDurationMs, + }) + .from(jobExecutionLogs) + .where(eq(jobExecutionLogs.scheduleId, job.id)) + .orderBy(desc(jobExecutionLogs.startedAt)) + .limit(5) + return execRows.length > 0 ? serializeRecentExecutions(execRows) : null }) } } diff --git a/package.json b/package.json index c552dbad165..ce15ab78107 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "trace-events-contract:check": "bun run scripts/sync-trace-events-contract.ts --check", "metrics-contract:generate": "bun run scripts/sync-metrics-contract.ts", "metrics-contract:check": "bun run scripts/sync-metrics-contract.ts --check", + "vfs-snapshot-contract:generate": "bun run scripts/sync-vfs-snapshot-contract.ts", + "vfs-snapshot-contract:check": "bun run scripts/sync-vfs-snapshot-contract.ts --check", "mship:generate": "bun run scripts/generate-mship-contracts.ts", "mship:check": "bun run scripts/generate-mship-contracts.ts --check", "prepare": "bun husky", diff --git a/scripts/generate-mship-contracts.ts b/scripts/generate-mship-contracts.ts index a789f0825f4..f0cdd9439bf 100644 --- a/scripts/generate-mship-contracts.ts +++ b/scripts/generate-mship-contracts.ts @@ -23,6 +23,7 @@ const GENERATORS = [ 'scripts/sync-trace-attribute-values-contract.ts', 'scripts/sync-trace-events-contract.ts', 'scripts/sync-metrics-contract.ts', + 'scripts/sync-vfs-snapshot-contract.ts', ] // Generated files under this path. We biome-format this whole dir on diff --git a/scripts/sync-vfs-snapshot-contract.ts b/scripts/sync-vfs-snapshot-contract.ts new file mode 100644 index 00000000000..e0e616ebdbe --- /dev/null +++ b/scripts/sync-vfs-snapshot-contract.ts @@ -0,0 +1,46 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { compile } from 'json-schema-to-typescript' +import { formatGeneratedSource } from './format-generated-source' + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(SCRIPT_DIR, '..') +// Matches the sibling sync scripts' canonical layout. In a repo where the Go +// service lives at `mothership/copilot`, pass `--input=` (e.g. +// `--input=../mothership/copilot/contracts/vfs-snapshot-v1.schema.json`). +const DEFAULT_CONTRACT_PATH = resolve( + ROOT, + '../copilot/copilot/contracts/vfs-snapshot-v1.schema.json' +) +const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts') + +async function main() { + const checkOnly = process.argv.includes('--check') + const inputPathArg = process.argv.find((arg) => arg.startsWith('--input=')) + const inputPath = inputPathArg + ? resolve(ROOT, inputPathArg.slice('--input='.length)) + : DEFAULT_CONTRACT_PATH + + const raw = await readFile(inputPath, 'utf8') + const schema = JSON.parse(raw) + const types = await compile(schema, 'VfsSnapshotV1', { + bannerComment: '// AUTO-GENERATED FILE. DO NOT EDIT.\n//', + unreachableDefinitions: true, + additionalProperties: false, + }) + const rendered = formatGeneratedSource(types, OUTPUT_PATH, ROOT) + + if (checkOnly) { + const existing = await readFile(OUTPUT_PATH, 'utf8').catch(() => null) + if (existing !== rendered) { + throw new Error('Generated vfs snapshot contract is stale. Run: bun run mship:generate') + } + return + } + + await mkdir(dirname(OUTPUT_PATH), { recursive: true }) + await writeFile(OUTPUT_PATH, rendered, 'utf8') +} + +await main() From 7349bf403ff754c8091eeff2aabe2ca5b606ff5c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 19 Jun 2026 15:53:12 -0700 Subject: [PATCH 13/16] feat(files): password, email-OTP, and SSO auth for public file shares (#5140) * feat(files): password, email-OTP, and SSO auth for public file shares * fix(files): suppress filename in share previews for email/sso, not just password * fix(files): normalize allow-list emails to lowercase; genericize shared SSO denial message * fix(security): make isEmailAllowed case-insensitive; normalize email at client gates * test(security): cover isEmailAllowed case-insensitive matching * fix(security): bind auth cookie to auth type; password endpoint rejects non-password shares * chore(db): format generated migration meta * fix(files): share upsert validation returns 400 not 500; disabling always succeeds * feat(access-control): org admins can restrict allowed file-share auth types --- apps/sim/app/api/chat/[identifier]/route.ts | 2 +- apps/sim/app/api/chat/utils.test.ts | 4 +- apps/sim/app/api/chat/utils.ts | 166 +- .../public/[token]/content/route.test.ts | 90 + .../api/files/public/[token]/content/route.ts | 15 + .../files/public/[token]/otp/route.test.ts | 171 + .../app/api/files/public/[token]/otp/route.ts | 194 + .../api/files/public/[token]/route.test.ts | 149 +- .../sim/app/api/files/public/[token]/route.ts | 95 +- .../files/public/[token]/sso/route.test.ts | 82 + .../app/api/files/public/[token]/sso/route.ts | 71 + apps/sim/app/api/proxy/tts/stream/route.ts | 5 +- apps/sim/app/api/speech/token/route.ts | 5 +- .../[id]/files/[fileId]/share/route.test.ts | 27 +- .../[id]/files/[fileId]/share/route.ts | 22 +- apps/sim/app/f/[token]/opengraph-image.tsx | 37 + apps/sim/app/f/[token]/page.tsx | 98 +- .../app/f/[token]/public-file-auth-shell.tsx | 43 + apps/sim/app/f/[token]/public-file-auth.tsx | 102 + .../app/f/[token]/public-file-email-auth.tsx | 217 + .../app/f/[token]/public-file-sso-auth.tsx | 103 + apps/sim/app/f/[token]/public-file-view.tsx | 5 +- apps/sim/app/f/[token]/utils.ts | 9 + .../components/share-modal/share-modal.tsx | 256 +- .../deploy-modal/components/chat/chat.tsx | 90 +- .../ui/generated-password-input.tsx | 118 + apps/sim/components/ui/index.ts | 1 + .../components/access-control.tsx | 64 + .../utils/permission-check.test.ts | 43 + .../access-control/utils/permission-check.ts | 27 +- apps/sim/hooks/queries/public-shares.ts | 68 +- .../lib/api/contracts/permission-groups.ts | 2 + apps/sim/lib/api/contracts/public-shares.ts | 127 +- apps/sim/lib/core/security/deployment-auth.ts | 202 + apps/sim/lib/core/security/deployment.test.ts | 24 + apps/sim/lib/core/security/deployment.ts | 35 +- apps/sim/lib/core/security/otp.ts | 8 +- apps/sim/lib/permission-groups/types.ts | 13 + apps/sim/lib/public-shares/share-manager.ts | 101 +- .../db/migrations/0245_public_share_auth.sql | 3 + .../db/migrations/meta/0245_snapshot.json | 16755 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 6 + scripts/check-api-validation-contracts.ts | 4 +- 44 files changed, 19298 insertions(+), 368 deletions(-) create mode 100644 apps/sim/app/api/files/public/[token]/content/route.test.ts create mode 100644 apps/sim/app/api/files/public/[token]/otp/route.test.ts create mode 100644 apps/sim/app/api/files/public/[token]/otp/route.ts create mode 100644 apps/sim/app/api/files/public/[token]/sso/route.test.ts create mode 100644 apps/sim/app/api/files/public/[token]/sso/route.ts create mode 100644 apps/sim/app/f/[token]/opengraph-image.tsx create mode 100644 apps/sim/app/f/[token]/public-file-auth-shell.tsx create mode 100644 apps/sim/app/f/[token]/public-file-auth.tsx create mode 100644 apps/sim/app/f/[token]/public-file-email-auth.tsx create mode 100644 apps/sim/app/f/[token]/public-file-sso-auth.tsx create mode 100644 apps/sim/app/f/[token]/utils.ts create mode 100644 apps/sim/components/ui/generated-password-input.tsx create mode 100644 apps/sim/lib/core/security/deployment-auth.ts create mode 100644 apps/sim/lib/core/security/deployment.test.ts create mode 100644 packages/db/migrations/0245_public_share_auth.sql create mode 100644 packages/db/migrations/meta/0245_snapshot.json diff --git a/apps/sim/app/api/chat/[identifier]/route.ts b/apps/sim/app/api/chat/[identifier]/route.ts index dc02c328436..2425df34db3 100644 --- a/apps/sim/app/api/chat/[identifier]/route.ts +++ b/apps/sim/app/api/chat/[identifier]/route.ts @@ -366,7 +366,7 @@ export const GET = withRouteHandler( deployment.authType !== 'public' && deployment.authType !== 'sso' && authCookie && - validateAuthToken(authCookie.value, deployment.id, deployment.password) + validateAuthToken(authCookie.value, deployment.id, deployment.authType, deployment.password) ) { return createSuccessResponse(toChatConfigResponse(deployment)) } diff --git a/apps/sim/app/api/chat/utils.test.ts b/apps/sim/app/api/chat/utils.test.ts index 0f0a5b6406a..a6eea9107e4 100644 --- a/apps/sim/app/api/chat/utils.test.ts +++ b/apps/sim/app/api/chat/utils.test.ts @@ -76,6 +76,7 @@ vi.mock('@/lib/core/security/deployment', () => ({ validateAuthToken: mockValidateAuthToken, setDeploymentAuthCookie: mockSetDeploymentAuthCookie, isEmailAllowed: mockIsEmailAllowed, + deploymentAuthCookieName: (prefix: string, id: string) => `${prefix}_auth_${id}`, })) vi.mock('@/lib/core/config/env-flags', () => ({ @@ -134,6 +135,7 @@ describe('Chat API Utils', () => { expect(mockValidateAuthToken).toHaveBeenCalledWith( 'valid-token', 'chat-id', + 'password', 'encrypted-password' ) expect(result.authorized).toBe(true) @@ -407,7 +409,7 @@ describe('Chat API Utils', () => { }) expect(result.authorized).toBe(false) - expect(result.error).toBe('Your email is not authorized to access this chat') + expect(result.error).toBe('Your email is not authorized to access this resource') }) }) }) diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 680f96d8024..c200a47adb5 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -2,37 +2,20 @@ import { db } from '@sim/db' import { chat, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' -import { safeCompare } from '@sim/security/compare' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest, NextResponse } from 'next/server' import { isWorkspaceApiExecutionEntitled } from '@/lib/billing/core/api-access' import { getEnv } from '@/lib/core/config/env' import { isBillingEnabled, isFreeApiDeploymentGateEnabled } from '@/lib/core/config/env-flags' -import type { TokenBucketConfig } from '@/lib/core/rate-limiter' -import { RateLimiter } from '@/lib/core/rate-limiter' +import { setDeploymentAuthCookie } from '@/lib/core/security/deployment' import { - isEmailAllowed, - setDeploymentAuthCookie, - validateAuthToken, -} from '@/lib/core/security/deployment' -import { decryptSecret } from '@/lib/core/security/encryption' -import { getClientIp } from '@/lib/core/utils/request' + type DeploymentAuthResult, + validateDeploymentAuth, +} from '@/lib/core/security/deployment-auth' import { createErrorResponse } from '@/app/api/workflows/utils' const logger = createLogger('ChatAuthUtils') -const rateLimiter = new RateLimiter() - -/** - * Throttles unauthenticated password guesses per client IP against a single - * deployment, mirroring the OTP/SSO IP limits. - */ -const PASSWORD_IP_RATE_LIMIT: TokenBucketConfig = { - maxTokens: 10, - refillRate: 10, - refillIntervalMs: 15 * 60_000, -} - export function setChatAuthCookie( response: NextResponse, chatId: string, @@ -157,144 +140,15 @@ export async function checkChatAccess( : { hasAccess: false } } +/** + * Validates auth for a deployed chat. Thin wrapper over the shared + * {@link validateDeploymentAuth} with the `'chat'` cookie/rate-limit namespace. + */ export async function validateChatAuth( requestId: string, deployment: any, request: NextRequest, parsedBody?: any -): Promise<{ authorized: boolean; error?: string; status?: number; retryAfterMs?: number }> { - const authType = deployment.authType || 'public' - - if (authType === 'public') { - return { authorized: true } - } - - if (authType !== 'sso') { - const cookieName = `chat_auth_${deployment.id}` - const authCookie = request.cookies.get(cookieName) - - if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) { - return { authorized: true } - } - } - - if (authType === 'password') { - if (request.method === 'GET') { - return { authorized: false, error: 'auth_required_password' } - } - - try { - if (!parsedBody) { - return { authorized: false, error: 'Password is required' } - } - - const { password, input } = parsedBody - - if (input && !password) { - return { authorized: false, error: 'auth_required_password' } - } - - if (!password) { - return { authorized: false, error: 'Password is required' } - } - - if (!deployment.password) { - logger.error(`[${requestId}] No password set for password-protected chat: ${deployment.id}`) - return { authorized: false, error: 'Authentication configuration error' } - } - - const ip = getClientIp(request) - const ipRateLimit = await rateLimiter.checkRateLimitDirect( - `chat-password:ip:${deployment.id}:${ip}`, - PASSWORD_IP_RATE_LIMIT - ) - if (!ipRateLimit.allowed) { - logger.warn( - `[${requestId}] Password attempt IP rate limit exceeded for chat ${deployment.id} from ${ip}` - ) - return { - authorized: false, - error: 'Too many attempts. Please try again later.', - status: 429, - retryAfterMs: ipRateLimit.retryAfterMs ?? PASSWORD_IP_RATE_LIMIT.refillIntervalMs, - } - } - - const { decrypted } = await decryptSecret(deployment.password) - if (!safeCompare(password, decrypted)) { - return { authorized: false, error: 'Invalid password' } - } - - return { authorized: true } - } catch (error) { - logger.error(`[${requestId}] Error validating password:`, error) - return { authorized: false, error: 'Authentication error' } - } - } - - if (authType === 'email') { - if (request.method === 'GET') { - return { authorized: false, error: 'auth_required_email' } - } - - try { - if (!parsedBody) { - return { authorized: false, error: 'Email is required' } - } - - const { email, input } = parsedBody - - if (input && !email) { - return { authorized: false, error: 'auth_required_email' } - } - - if (!email) { - return { authorized: false, error: 'Email is required' } - } - - const allowedEmails = deployment.allowedEmails || [] - - if (isEmailAllowed(email, allowedEmails)) { - return { authorized: false, error: 'otp_required' } - } - - return { authorized: false, error: 'Email not authorized' } - } catch (error) { - logger.error(`[${requestId}] Error validating email:`, error) - return { authorized: false, error: 'Authentication error' } - } - } - - if (authType === 'sso') { - try { - if (request.method !== 'GET' && !parsedBody) { - return { authorized: false, error: 'SSO authentication is required' } - } - - const { getSession } = await import('@/lib/auth') - const session = await getSession() - - if (!session || !session.user) { - return { authorized: false, error: 'auth_required_sso' } - } - - const userEmail = session.user.email - if (!userEmail) { - return { authorized: false, error: 'SSO session does not contain email' } - } - - const allowedEmails = deployment.allowedEmails || [] - - if (isEmailAllowed(userEmail, allowedEmails)) { - return { authorized: true } - } - - return { authorized: false, error: 'Your email is not authorized to access this chat' } - } catch (error) { - logger.error(`[${requestId}] Error validating SSO:`, error) - return { authorized: false, error: 'SSO authentication error' } - } - } - - return { authorized: false, error: 'Unsupported authentication type' } +): Promise { + return validateDeploymentAuth(requestId, deployment, request, parsedBody, 'chat') } diff --git a/apps/sim/app/api/files/public/[token]/content/route.test.ts b/apps/sim/app/api/files/public/[token]/content/route.test.ts new file mode 100644 index 00000000000..251f96f83e3 --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/content/route.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockResolveActiveShareByToken, + mockEnforceRateLimit, + mockValidateDeploymentAuth, + mockDownloadFile, + mockResolveServableDoc, +} = vi.hoisted(() => ({ + mockResolveActiveShareByToken: vi.fn(), + mockEnforceRateLimit: vi.fn(), + mockValidateDeploymentAuth: vi.fn(), + mockDownloadFile: vi.fn(), + mockResolveServableDoc: vi.fn(), +})) + +vi.mock('@/lib/public-shares/share-manager', () => ({ + resolveActiveShareByToken: mockResolveActiveShareByToken, +})) + +vi.mock('@/lib/public-shares/rate-limit', () => ({ + enforcePublicFileRateLimit: mockEnforceRateLimit, +})) + +vi.mock('@/lib/core/security/deployment-auth', () => ({ + validateDeploymentAuth: mockValidateDeploymentAuth, +})) + +vi.mock('@/lib/uploads/core/storage-service', () => ({ + downloadFile: mockDownloadFile, +})) + +vi.mock('@/lib/copilot/tools/server/files/doc-compile', () => ({ + resolveServableDoc: mockResolveServableDoc, +})) + +import { GET } from '@/app/api/files/public/[token]/content/route' + +const params = (token = 'tok_1') => ({ params: Promise.resolve({ token }) }) +const request = (token = 'tok_1') => + new NextRequest(`http://localhost/api/files/public/${token}/content`) + +const passwordShare = { + share: { id: 'sh_1', token: 'tok_1', authType: 'password', password: 'enc:secret' }, + file: { + id: 'wf_1', + key: 'workspace/ws/secret-key.pdf', + workspaceId: 'ws-1', + originalName: 'report.pdf', + contentType: 'application/pdf', + size: 4, + }, + workspaceName: 'Acme', + ownerName: 'Jane', +} + +describe('GET /api/files/public/[token]/content', () => { + beforeEach(() => { + vi.clearAllMocks() + mockEnforceRateLimit.mockResolvedValue(null) + mockResolveActiveShareByToken.mockResolvedValue(passwordShare) + mockDownloadFile.mockResolvedValue(Buffer.from('data')) + mockResolveServableDoc.mockResolvedValue({ kind: 'passthrough' }) + }) + + it('returns 401 and never reads storage when a password share is unauthorized', async () => { + mockValidateDeploymentAuth.mockResolvedValueOnce({ + authorized: false, + error: 'auth_required_password', + }) + const res = await GET(request(), params()) + expect(res.status).toBe(401) + expect((await res.json()).error).toBe('auth_required_password') + expect(mockDownloadFile).not.toHaveBeenCalled() + }) + + it('serves the bytes once authorized', async () => { + mockValidateDeploymentAuth.mockResolvedValueOnce({ authorized: true }) + const res = await GET(request(), params()) + expect(res.status).toBe(200) + expect(mockDownloadFile).toHaveBeenCalledWith({ + key: passwordShare.file.key, + context: 'workspace', + }) + }) +}) diff --git a/apps/sim/app/api/files/public/[token]/content/route.ts b/apps/sim/app/api/files/public/[token]/content/route.ts index 8c8e04a0dfe..8db42e412bd 100644 --- a/apps/sim/app/api/files/public/[token]/content/route.ts +++ b/apps/sim/app/api/files/public/[token]/content/route.ts @@ -4,6 +4,8 @@ import { NextResponse } from 'next/server' import { getPublicFileContentContract } from '@/lib/api/contracts/public-shares' import { parseRequest } from '@/lib/api/server' import { resolveServableDoc } from '@/lib/copilot/tools/server/files/doc-compile' +import { validateDeploymentAuth } from '@/lib/core/security/deployment-auth' +import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit' import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' @@ -28,6 +30,8 @@ const logger = createLogger('PublicFileContentAPI') */ export const GET = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + const requestId = generateRequestId() + try { const limited = await enforcePublicFileRateLimit(request, 'content') if (limited) return limited @@ -41,6 +45,17 @@ export const GET = withRouteHandler( throw new FileNotFoundError('Not found') } + const auth = await validateDeploymentAuth( + requestId, + resolved.share, + request, + undefined, + 'file' + ) + if (!auth.authorized) { + return NextResponse.json({ error: auth.error ?? 'auth_required_password' }, { status: 401 }) + } + const { file } = resolved const raw = await downloadFile({ key: file.key, context: 'workspace' }) diff --git a/apps/sim/app/api/files/public/[token]/otp/route.test.ts b/apps/sim/app/api/files/public/[token]/otp/route.test.ts new file mode 100644 index 00000000000..eb363eb7d5f --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/otp/route.test.ts @@ -0,0 +1,171 @@ +/** + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockResolveActiveShareByToken, + mockIsEmailAllowed, + mockSetDeploymentAuthCookie, + mockGenerateOTP, + mockStoreOTP, + mockGetOTP, + mockDeleteOTP, + mockIncrementOTPAttempts, + mockDecodeOTPValue, + mockRenderOTPEmail, + mockSendEmail, + mockCheckRateLimitDirect, +} = vi.hoisted(() => ({ + mockResolveActiveShareByToken: vi.fn(), + mockIsEmailAllowed: vi.fn(), + mockSetDeploymentAuthCookie: vi.fn(), + mockGenerateOTP: vi.fn(), + mockStoreOTP: vi.fn(), + mockGetOTP: vi.fn(), + mockDeleteOTP: vi.fn(), + mockIncrementOTPAttempts: vi.fn(), + mockDecodeOTPValue: vi.fn(), + mockRenderOTPEmail: vi.fn(), + mockSendEmail: vi.fn(), + mockCheckRateLimitDirect: vi.fn(), +})) + +vi.mock('@/lib/public-shares/share-manager', () => ({ + resolveActiveShareByToken: mockResolveActiveShareByToken, +})) +vi.mock('@/lib/core/security/deployment', () => ({ + isEmailAllowed: mockIsEmailAllowed, + setDeploymentAuthCookie: mockSetDeploymentAuthCookie, +})) +vi.mock('@/lib/core/security/otp', () => ({ + generateOTP: mockGenerateOTP, + storeOTP: mockStoreOTP, + getOTP: mockGetOTP, + deleteOTP: mockDeleteOTP, + incrementOTPAttempts: mockIncrementOTPAttempts, + decodeOTPValue: mockDecodeOTPValue, + MAX_OTP_ATTEMPTS: 5, + OTP_IP_RATE_LIMIT: { maxTokens: 10, refillRate: 10, refillIntervalMs: 1000 }, + OTP_EMAIL_RATE_LIMIT: { maxTokens: 3, refillRate: 3, refillIntervalMs: 1000 }, +})) +vi.mock('@/components/emails', () => ({ renderOTPEmail: mockRenderOTPEmail })) +vi.mock('@/lib/messaging/email/mailer', () => ({ sendEmail: mockSendEmail })) +vi.mock('@/lib/core/rate-limiter', () => ({ + RateLimiter: class { + checkRateLimitDirect = mockCheckRateLimitDirect + }, +})) + +import { POST, PUT } from '@/app/api/files/public/[token]/otp/route' + +const params = (token = 'tok_1') => ({ params: Promise.resolve({ token }) }) +const post = (email: string, token = 'tok_1') => + new NextRequest(`http://localhost/api/files/public/${token}/otp`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email }), + }) +const put = (email: string, otp: string, token = 'tok_1') => + new NextRequest(`http://localhost/api/files/public/${token}/otp`, { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email, otp }), + }) + +const emailShare = { + share: { id: 'sh_1', authType: 'email', password: null, allowedEmails: ['@acme.com'] }, + file: { originalName: 'report.pdf' }, +} + +describe('POST /api/files/public/[token]/otp', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCheckRateLimitDirect.mockResolvedValue({ allowed: true }) + mockResolveActiveShareByToken.mockResolvedValue(emailShare) + mockIsEmailAllowed.mockReturnValue(true) + mockGenerateOTP.mockReturnValue('123456') + mockRenderOTPEmail.mockResolvedValue('') + mockSendEmail.mockResolvedValue({ success: true }) + }) + + it('sends a code to an allow-listed email', async () => { + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(200) + expect(mockStoreOTP).toHaveBeenCalledWith('file', 'sh_1', 'user@acme.com', '123456') + expect(mockSendEmail).toHaveBeenCalled() + }) + + it('rejects an email not on the allow-list with 403', async () => { + mockIsEmailAllowed.mockReturnValueOnce(false) + const res = await POST(post('user@evil.com'), params()) + expect(res.status).toBe(403) + expect(mockStoreOTP).not.toHaveBeenCalled() + }) + + it('lowercases the email for allow-list matching and OTP storage', async () => { + await POST(post('User@ACME.com'), params()) + expect(mockIsEmailAllowed).toHaveBeenCalledWith('user@acme.com', expect.anything()) + expect(mockStoreOTP).toHaveBeenCalledWith('file', 'sh_1', 'user@acme.com', '123456') + }) + + it('rejects a non-email share with 400', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce({ + ...emailShare, + share: { ...emailShare.share, authType: 'password' }, + }) + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(400) + }) + + it('returns 429 when the IP rate limit is exceeded', async () => { + mockCheckRateLimitDirect.mockResolvedValueOnce({ allowed: false, retryAfterMs: 1000 }) + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(429) + expect(res.headers.get('Retry-After')).toBe('1') + }) +}) + +describe('PUT /api/files/public/[token]/otp', () => { + beforeEach(() => { + vi.clearAllMocks() + mockResolveActiveShareByToken.mockResolvedValue(emailShare) + mockGetOTP.mockResolvedValue('123456:0') + mockDecodeOTPValue.mockReturnValue({ otp: '123456', attempts: 0 }) + }) + + it('verifies a correct code, sets the cookie, returns authType', async () => { + const res = await PUT(put('user@acme.com', '123456'), params()) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ authType: 'email' }) + expect(mockDeleteOTP).toHaveBeenCalledWith('file', 'sh_1', 'user@acme.com') + expect(mockSetDeploymentAuthCookie).toHaveBeenCalledWith( + expect.anything(), + 'file', + 'sh_1', + 'email', + null + ) + }) + + it('rejects a wrong code with 400 and increments attempts', async () => { + mockIncrementOTPAttempts.mockResolvedValueOnce('incremented') + const res = await PUT(put('user@acme.com', '000000'), params()) + expect(res.status).toBe(400) + expect(mockIncrementOTPAttempts).toHaveBeenCalled() + expect(mockSetDeploymentAuthCookie).not.toHaveBeenCalled() + }) + + it('returns 429 when attempts are exhausted on a wrong code', async () => { + mockIncrementOTPAttempts.mockResolvedValueOnce('locked') + const res = await PUT(put('user@acme.com', '000000'), params()) + expect(res.status).toBe(429) + }) + + it('returns 400 when no code was issued', async () => { + mockGetOTP.mockResolvedValueOnce(null) + const res = await PUT(put('user@acme.com', '123456'), params()) + expect(res.status).toBe(400) + }) +}) diff --git a/apps/sim/app/api/files/public/[token]/otp/route.ts b/apps/sim/app/api/files/public/[token]/otp/route.ts new file mode 100644 index 00000000000..c7257db0d12 --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/otp/route.ts @@ -0,0 +1,194 @@ +import { createLogger } from '@sim/logger' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { renderOTPEmail } from '@/components/emails' +import { + requestPublicFileOtpContract, + verifyPublicFileOtpContract, +} from '@/lib/api/contracts/public-shares' +import { parseRequest } from '@/lib/api/server' +import { RateLimiter } from '@/lib/core/rate-limiter' +import { isEmailAllowed, setDeploymentAuthCookie } from '@/lib/core/security/deployment' +import { + decodeOTPValue, + deleteOTP, + generateOTP, + getOTP, + incrementOTPAttempts, + MAX_OTP_ATTEMPTS, + OTP_EMAIL_RATE_LIMIT, + OTP_IP_RATE_LIMIT, + storeOTP, +} from '@/lib/core/security/otp' +import { generateRequestId, getClientIp } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { sendEmail } from '@/lib/messaging/email/mailer' +import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('PublicFileOtpAPI') + +const rateLimiter = new RateLimiter() + +const SHARE_EMAIL_LABEL = 'a shared file' + +/** Allow-list for an email-gated share, read off the resolved row. */ +function shareAllowedEmails(allowedEmails: unknown): string[] { + return Array.isArray(allowedEmails) ? (allowedEmails as string[]) : [] +} + +function rateLimited(retryAfterMs: number | undefined, fallbackMs: number): NextResponse { + const response = NextResponse.json( + { error: 'Too many requests. Please try again later.' }, + { status: 429 } + ) + response.headers.set('Retry-After', String(Math.ceil((retryAfterMs ?? fallbackMs) / 1000))) + return response +} + +/** + * POST /api/files/public/[token]/otp + * Sends a 6-digit verification code to an allow-listed email for an email-gated share. + */ +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + const requestId = generateRequestId() + + try { + const ip = getClientIp(request) + const ipRateLimit = await rateLimiter.checkRateLimitDirect( + `file-otp:ip:${ip}`, + OTP_IP_RATE_LIMIT + ) + if (!ipRateLimit.allowed) { + logger.warn(`[${requestId}] OTP IP rate limit exceeded from ${ip}`) + return rateLimited(ipRateLimit.retryAfterMs, OTP_IP_RATE_LIMIT.refillIntervalMs) + } + + const parsed = await parseRequest(requestPublicFileOtpContract, request, context) + if (!parsed.success) return parsed.response + const { token } = parsed.data.params + // Normalize once so allow-list matching, OTP storage, and the verify lookup + // all key off the same value (allow-list entries are stored lowercase). + const email = parsed.data.body.email.trim().toLowerCase() + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + if (resolved.share.authType !== 'email') { + return NextResponse.json( + { error: 'This file does not use email authentication' }, + { status: 400 } + ) + } + + if (!isEmailAllowed(email, shareAllowedEmails(resolved.share.allowedEmails))) { + return NextResponse.json({ error: 'Email not authorized for this file' }, { status: 403 }) + } + + const emailRateLimit = await rateLimiter.checkRateLimitDirect( + `file-otp:email:${resolved.share.id}:${email}`, + OTP_EMAIL_RATE_LIMIT + ) + if (!emailRateLimit.allowed) { + logger.warn(`[${requestId}] OTP email rate limit exceeded for ${email}`) + return rateLimited(emailRateLimit.retryAfterMs, OTP_EMAIL_RATE_LIMIT.refillIntervalMs) + } + + const otp = generateOTP() + await storeOTP('file', resolved.share.id, email, otp) + + const emailHtml = await renderOTPEmail(otp, email, 'email-verification', SHARE_EMAIL_LABEL) + const emailResult = await sendEmail({ + to: email, + subject: `Verification code for ${SHARE_EMAIL_LABEL}`, + html: emailHtml, + }) + if (!emailResult.success) { + logger.error(`[${requestId}] Failed to send OTP email:`, emailResult.message) + return NextResponse.json({ error: 'Failed to send verification email' }, { status: 500 }) + } + + logger.info(`[${requestId}] OTP sent for share ${resolved.share.id}`) + return NextResponse.json({ message: 'Verification code sent' }) + } catch (error) { + logger.error(`[${requestId}] Error processing OTP request:`, error) + return NextResponse.json({ error: 'Failed to process request' }, { status: 500 }) + } + } +) + +/** + * PUT /api/files/public/[token]/otp + * Verifies the code and, on success, sets the `file_auth_{shareId}` cookie. + */ +export const PUT = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + const requestId = generateRequestId() + + try { + const parsed = await parseRequest(verifyPublicFileOtpContract, request, context) + if (!parsed.success) return parsed.response + const { token } = parsed.data.params + const { otp } = parsed.data.body + const email = parsed.data.body.email.trim().toLowerCase() + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + if (resolved.share.authType !== 'email') { + return NextResponse.json( + { error: 'This file does not use email authentication' }, + { status: 400 } + ) + } + + const storedValue = await getOTP('file', resolved.share.id, email) + if (!storedValue) { + return NextResponse.json( + { error: 'No verification code found, request a new one' }, + { status: 400 } + ) + } + + const { otp: storedOTP, attempts } = decodeOTPValue(storedValue) + if (attempts >= MAX_OTP_ATTEMPTS) { + await deleteOTP('file', resolved.share.id, email) + return NextResponse.json( + { error: 'Too many failed attempts. Please request a new code.' }, + { status: 429 } + ) + } + + if (storedOTP !== otp) { + const result = await incrementOTPAttempts('file', resolved.share.id, email, storedValue) + if (result === 'locked') { + return NextResponse.json( + { error: 'Too many failed attempts. Please request a new code.' }, + { status: 429 } + ) + } + return NextResponse.json({ error: 'Invalid verification code' }, { status: 400 }) + } + + await deleteOTP('file', resolved.share.id, email) + + const response = NextResponse.json({ authType: resolved.share.authType }) + setDeploymentAuthCookie( + response, + 'file', + resolved.share.id, + resolved.share.authType, + resolved.share.password + ) + logger.info(`[${requestId}] OTP verified for share ${resolved.share.id}`) + return response + } catch (error) { + logger.error(`[${requestId}] Error verifying OTP:`, error) + return NextResponse.json({ error: 'Failed to process request' }, { status: 500 }) + } + } +) diff --git a/apps/sim/app/api/files/public/[token]/route.test.ts b/apps/sim/app/api/files/public/[token]/route.test.ts index e3d78316eba..aa32176c87c 100644 --- a/apps/sim/app/api/files/public/[token]/route.test.ts +++ b/apps/sim/app/api/files/public/[token]/route.test.ts @@ -4,9 +4,16 @@ import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockResolveActiveShareByToken, mockEnforceRateLimit } = vi.hoisted(() => ({ +const { + mockResolveActiveShareByToken, + mockEnforceRateLimit, + mockValidateDeploymentAuth, + mockSetDeploymentAuthCookie, +} = vi.hoisted(() => ({ mockResolveActiveShareByToken: vi.fn(), mockEnforceRateLimit: vi.fn(), + mockValidateDeploymentAuth: vi.fn(), + mockSetDeploymentAuthCookie: vi.fn(), })) vi.mock('@/lib/public-shares/share-manager', () => ({ @@ -17,16 +24,50 @@ vi.mock('@/lib/public-shares/rate-limit', () => ({ enforcePublicFileRateLimit: mockEnforceRateLimit, })) +vi.mock('@/lib/core/security/deployment-auth', () => ({ + validateDeploymentAuth: mockValidateDeploymentAuth, +})) + +vi.mock('@/lib/core/security/deployment', () => ({ + setDeploymentAuthCookie: mockSetDeploymentAuthCookie, +})) + import { NextResponse } from 'next/server' -import { GET } from '@/app/api/files/public/[token]/route' +import { GET, POST } from '@/app/api/files/public/[token]/route' const params = (token = 'tok_1') => ({ params: Promise.resolve({ token }) }) const request = (token = 'tok_1') => new NextRequest(`http://localhost/api/files/public/${token}`) +const postRequest = (password: string, token = 'tok_1') => + new NextRequest(`http://localhost/api/files/public/${token}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ password }), + }) + +const publicShare = { + share: { id: 'sh_1', token: 'tok_1', authType: 'public', password: null }, + file: { + id: 'wf_1', + key: 'workspace/ws/secret-key.pdf', + workspaceId: 'ws-secret', + originalName: 'report.pdf', + contentType: 'application/pdf', + size: 2048, + }, + workspaceName: 'Acme Workspace', + ownerName: 'Jane Doe', +} + +const passwordShare = { + ...publicShare, + share: { id: 'sh_1', token: 'tok_1', authType: 'password', password: 'enc:secret' }, +} describe('GET /api/files/public/[token]', () => { beforeEach(() => { vi.clearAllMocks() mockEnforceRateLimit.mockResolvedValue(null) // allow by default + mockValidateDeploymentAuth.mockResolvedValue({ authorized: true }) // public by default }) it('returns 429 when the per-IP rate limit is exceeded', async () => { @@ -44,20 +85,8 @@ describe('GET /api/files/public/[token]', () => { expect(res.status).toBe(404) }) - it('returns public-safe metadata (name/type/size + provenance) without leaking the key or workspace id', async () => { - mockResolveActiveShareByToken.mockResolvedValueOnce({ - share: { id: 'sh_1', token: 'tok_1' }, - file: { - id: 'wf_1', - key: 'workspace/ws/secret-key.pdf', - workspaceId: 'ws-secret', - originalName: 'report.pdf', - contentType: 'application/pdf', - size: 2048, - }, - workspaceName: 'Acme Workspace', - ownerName: 'Jane Doe', - }) + it('returns public-safe metadata without leaking the key or workspace id', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce(publicShare) const res = await GET(request(), params()) expect(res.status).toBe(200) const body = await res.json() @@ -72,4 +101,92 @@ describe('GET /api/files/public/[token]', () => { expect(JSON.stringify(body)).not.toContain('secret-key') expect(JSON.stringify(body)).not.toContain('ws-secret') }) + + it('returns 401 auth_required_password for a password share without a valid cookie', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce(passwordShare) + mockValidateDeploymentAuth.mockResolvedValueOnce({ + authorized: false, + error: 'auth_required_password', + }) + const res = await GET(request(), params()) + expect(res.status).toBe(401) + expect((await res.json()).error).toBe('auth_required_password') + expect(mockValidateDeploymentAuth).toHaveBeenCalledWith( + expect.any(String), + passwordShare.share, + expect.anything(), + undefined, + 'file' + ) + }) + + it('serves metadata for a password share once authorized by cookie', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce(passwordShare) + mockValidateDeploymentAuth.mockResolvedValueOnce({ authorized: true }) + const res = await GET(request(), params()) + expect(res.status).toBe(200) + expect((await res.json()).name).toBe('report.pdf') + }) +}) + +describe('POST /api/files/public/[token]', () => { + beforeEach(() => { + vi.clearAllMocks() + mockResolveActiveShareByToken.mockResolvedValue(passwordShare) + }) + + it('sets the file_auth cookie and returns the authType on a correct password', async () => { + mockValidateDeploymentAuth.mockResolvedValueOnce({ authorized: true }) + const res = await POST(postRequest('hunter2'), params()) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ authType: 'password' }) + expect(mockSetDeploymentAuthCookie).toHaveBeenCalledWith( + expect.anything(), + 'file', + 'sh_1', + 'password', + 'enc:secret' + ) + }) + + it('refuses to mint a cookie for a non-password (e.g. public) share', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce({ + ...passwordShare, + share: { id: 'sh_1', token: 'tok_1', authType: 'public', password: null }, + }) + const res = await POST(postRequest('whatever'), params()) + expect(res.status).toBe(400) + expect(mockValidateDeploymentAuth).not.toHaveBeenCalled() + expect(mockSetDeploymentAuthCookie).not.toHaveBeenCalled() + }) + + it('returns 401 Invalid password on mismatch without setting a cookie', async () => { + mockValidateDeploymentAuth.mockResolvedValueOnce({ + authorized: false, + error: 'Invalid password', + }) + const res = await POST(postRequest('wrong'), params()) + expect(res.status).toBe(401) + expect((await res.json()).error).toBe('Invalid password') + expect(mockSetDeploymentAuthCookie).not.toHaveBeenCalled() + }) + + it('returns 429 with Retry-After when password attempts are rate-limited', async () => { + mockValidateDeploymentAuth.mockResolvedValueOnce({ + authorized: false, + error: 'Too many attempts. Please try again later.', + status: 429, + retryAfterMs: 60_000, + }) + const res = await POST(postRequest('wrong'), params()) + expect(res.status).toBe(429) + expect(res.headers.get('Retry-After')).toBe('60') + expect(mockSetDeploymentAuthCookie).not.toHaveBeenCalled() + }) + + it('returns 404 for an unknown token', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce(null) + const res = await POST(postRequest('hunter2'), params()) + expect(res.status).toBe(404) + }) }) diff --git a/apps/sim/app/api/files/public/[token]/route.ts b/apps/sim/app/api/files/public/[token]/route.ts index afc99e22b54..5c4482b22a9 100644 --- a/apps/sim/app/api/files/public/[token]/route.ts +++ b/apps/sim/app/api/files/public/[token]/route.ts @@ -2,8 +2,14 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import { getPublicFileContract } from '@/lib/api/contracts/public-shares' +import { + authenticatePublicFileContract, + getPublicFileContract, +} from '@/lib/api/contracts/public-shares' import { parseRequest } from '@/lib/api/server' +import { setDeploymentAuthCookie } from '@/lib/core/security/deployment' +import { validateDeploymentAuth } from '@/lib/core/security/deployment-auth' +import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit' import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' @@ -15,10 +21,14 @@ const logger = createLogger('PublicFileMetadataAPI') /** * GET /api/files/public/[token] * Public, unauthenticated metadata for a shared file. Returns 404 for unknown, - * inactive, or deleted shares — the existence of a file is never leaked. + * inactive, or deleted shares — the existence of a file is never leaked. A + * password-protected share returns 401 `auth_required_password` until a valid + * `file_auth_{shareId}` cookie is present. */ export const GET = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + const requestId = generateRequestId() + try { const limited = await enforcePublicFileRateLimit(request, 'metadata') if (limited) return limited @@ -32,6 +42,17 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Not found' }, { status: 404 }) } + const auth = await validateDeploymentAuth( + requestId, + resolved.share, + request, + undefined, + 'file' + ) + if (!auth.authorized) { + return NextResponse.json({ error: auth.error ?? 'auth_required_password' }, { status: 401 }) + } + const { file, workspaceName, ownerName } = resolved return NextResponse.json({ token, @@ -50,3 +71,73 @@ export const GET = withRouteHandler( } } ) + +/** + * POST /api/files/public/[token] + * Exchanges a share password for a `file_auth_{shareId}` cookie. IP rate-limited + * via the shared deployment-auth gate; returns 401 (`Invalid password`) on + * mismatch and 429 (with `Retry-After`) when throttled. + */ +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + const requestId = generateRequestId() + + try { + const parsed = await parseRequest(authenticatePublicFileContract, request, context) + if (!parsed.success) return parsed.response + const { token } = parsed.data.params + const { password } = parsed.data.body + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + // This endpoint authenticates password shares only. Refusing other modes + // here prevents minting a `file_auth` cookie for a `public` share (which + // `validateDeploymentAuth` would otherwise authorize), which could later + // satisfy the gate if the share is switched to `email`/`sso`. + if (resolved.share.authType !== 'password') { + return NextResponse.json( + { error: 'This file does not use password authentication' }, + { status: 400 } + ) + } + + const auth = await validateDeploymentAuth( + requestId, + resolved.share, + request, + { password }, + 'file' + ) + if (!auth.authorized) { + const response = NextResponse.json( + { error: auth.error ?? 'Invalid password' }, + { status: auth.status ?? 401 } + ) + if (auth.status === 429 && auth.retryAfterMs !== undefined) { + response.headers.set('Retry-After', String(Math.ceil(auth.retryAfterMs / 1000))) + } + return response + } + + const response = NextResponse.json({ authType: resolved.share.authType }) + setDeploymentAuthCookie( + response, + 'file', + resolved.share.id, + resolved.share.authType, + resolved.share.password + ) + logger.info('Public file share password accepted', { token, shareId: resolved.share.id }) + return response + } catch (error) { + logger.error('Error authenticating public file share:', error) + return NextResponse.json( + { error: getErrorMessage(error, 'Failed to authenticate') }, + { status: 500 } + ) + } + } +) diff --git a/apps/sim/app/api/files/public/[token]/sso/route.test.ts b/apps/sim/app/api/files/public/[token]/sso/route.test.ts new file mode 100644 index 00000000000..92d78cd8b13 --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/sso/route.test.ts @@ -0,0 +1,82 @@ +/** + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockResolveActiveShareByToken, mockIsEmailAllowed, mockCheckRateLimitDirect } = vi.hoisted( + () => ({ + mockResolveActiveShareByToken: vi.fn(), + mockIsEmailAllowed: vi.fn(), + mockCheckRateLimitDirect: vi.fn(), + }) +) + +vi.mock('@/lib/public-shares/share-manager', () => ({ + resolveActiveShareByToken: mockResolveActiveShareByToken, +})) +vi.mock('@/lib/core/security/deployment', () => ({ isEmailAllowed: mockIsEmailAllowed })) +vi.mock('@/lib/core/rate-limiter', () => ({ + RateLimiter: class { + checkRateLimitDirect = mockCheckRateLimitDirect + }, +})) + +import { POST } from '@/app/api/files/public/[token]/sso/route' + +const params = (token = 'tok_1') => ({ params: Promise.resolve({ token }) }) +const post = (email: string, token = 'tok_1') => + new NextRequest(`http://localhost/api/files/public/${token}/sso`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email }), + }) + +const ssoShare = { + share: { id: 'sh_1', authType: 'sso', password: null, allowedEmails: ['@acme.com'] }, + file: { originalName: 'report.pdf' }, +} + +describe('POST /api/files/public/[token]/sso', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCheckRateLimitDirect.mockResolvedValue({ allowed: true }) + mockResolveActiveShareByToken.mockResolvedValue(ssoShare) + }) + + it('returns eligible:true for an allow-listed email', async () => { + mockIsEmailAllowed.mockReturnValueOnce(true) + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ eligible: true }) + }) + + it('returns eligible:false for a non-listed email', async () => { + mockIsEmailAllowed.mockReturnValueOnce(false) + const res = await POST(post('user@evil.com'), params()) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ eligible: false }) + }) + + it('rejects a non-sso share with 400', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce({ + ...ssoShare, + share: { ...ssoShare.share, authType: 'email' }, + }) + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(400) + }) + + it('returns 404 for an unknown token', async () => { + mockResolveActiveShareByToken.mockResolvedValueOnce(null) + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(404) + }) + + it('returns 429 when rate-limited', async () => { + mockCheckRateLimitDirect.mockResolvedValueOnce({ allowed: false, retryAfterMs: 2000 }) + const res = await POST(post('user@acme.com'), params()) + expect(res.status).toBe(429) + expect(res.headers.get('Retry-After')).toBe('2') + }) +}) diff --git a/apps/sim/app/api/files/public/[token]/sso/route.ts b/apps/sim/app/api/files/public/[token]/sso/route.ts new file mode 100644 index 00000000000..508c94777ab --- /dev/null +++ b/apps/sim/app/api/files/public/[token]/sso/route.ts @@ -0,0 +1,71 @@ +import { createLogger } from '@sim/logger' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { publicFileSSOContract } from '@/lib/api/contracts/public-shares' +import { parseRequest } from '@/lib/api/server' +import type { TokenBucketConfig } from '@/lib/core/rate-limiter' +import { RateLimiter } from '@/lib/core/rate-limiter' +import { isEmailAllowed } from '@/lib/core/security/deployment' +import { generateRequestId, getClientIp } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' + +const logger = createLogger('PublicFileSSOAPI') + +const rateLimiter = new RateLimiter() + +const SSO_IP_RATE_LIMIT: TokenBucketConfig = { + maxTokens: 20, + refillRate: 20, + refillIntervalMs: 15 * 60_000, +} + +/** + * POST /api/files/public/[token]/sso + * Reports whether an email is on the allow-list for an SSO-gated share. The actual + * authentication is the global Sim session (checked at the page/route gate). + */ +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ token: string }> }) => { + const requestId = generateRequestId() + + const ip = getClientIp(request) + const ipRateLimit = await rateLimiter.checkRateLimitDirect( + `file-sso:ip:${ip}`, + SSO_IP_RATE_LIMIT + ) + if (!ipRateLimit.allowed) { + logger.warn(`[${requestId}] SSO eligibility rate limit exceeded from ${ip}`) + const response = NextResponse.json( + { error: 'Too many requests. Please try again later.' }, + { status: 429 } + ) + response.headers.set( + 'Retry-After', + String(Math.ceil((ipRateLimit.retryAfterMs ?? SSO_IP_RATE_LIMIT.refillIntervalMs) / 1000)) + ) + return response + } + + const parsed = await parseRequest(publicFileSSOContract, request, context) + if (!parsed.success) return parsed.response + const { token } = parsed.data.params + const email = parsed.data.body.email.trim().toLowerCase() + + const resolved = await resolveActiveShareByToken(token) + if (!resolved) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + if (resolved.share.authType !== 'sso') { + return NextResponse.json({ error: 'This file is not configured for SSO' }, { status: 400 }) + } + + const allowedEmails = Array.isArray(resolved.share.allowedEmails) + ? (resolved.share.allowedEmails as string[]) + : [] + return NextResponse.json({ eligible: isEmailAllowed(email, allowedEmails) }) + } +) diff --git a/apps/sim/app/api/proxy/tts/stream/route.ts b/apps/sim/app/api/proxy/tts/stream/route.ts index 39d561522a9..c9a91675a5e 100644 --- a/apps/sim/app/api/proxy/tts/stream/route.ts +++ b/apps/sim/app/api/proxy/tts/stream/route.ts @@ -43,7 +43,10 @@ async function validateChatAuth(request: NextRequest, chatId: string): Promise ({ getWorkspaceFile: mockGetWorkspaceFile, })) -vi.mock('@/lib/public-shares/share-manager', () => ({ - getShareForResource: mockGetShareForResource, - upsertFileShare: mockUpsertFileShare, -})) +vi.mock('@/lib/public-shares/share-manager', () => { + class ShareValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'ShareValidationError' + } + } + return { + getShareForResource: mockGetShareForResource, + upsertFileShare: mockUpsertFileShare, + ShareValidationError, + } +}) vi.mock('@/ee/access-control/utils/permission-check', () => { class PublicFileSharingNotAllowedError extends Error { @@ -38,6 +47,7 @@ vi.mock('@sim/audit', () => auditMock) const WS = '7727ef3f-8cf6-4686-b063-2bb006a10785' const FILE_ID = 'wf_abc' +import { ShareValidationError } from '@/lib/public-shares/share-manager' import { GET, PUT } from '@/app/api/workspaces/[id]/files/[fileId]/share/route' const params = (id = WS, fileId = FILE_ID) => ({ params: Promise.resolve({ id, fileId }) }) @@ -102,6 +112,15 @@ describe('share route', () => { expect(mockUpsertFileShare).not.toHaveBeenCalled() }) + it('maps a ShareValidationError to 400, not 500', async () => { + mockUpsertFileShare.mockRejectedValueOnce( + new ShareValidationError('Password is required for password-protected shares') + ) + const res = await PUT(putRequest({ isActive: true, authType: 'password' }), params()) + expect(res.status).toBe(400) + expect((await res.json()).error).toBe('Password is required for password-protected shares') + }) + it('returns 404 when the file is not in the workspace', async () => { mockGetWorkspaceFile.mockResolvedValueOnce(null) const res = await PUT(putRequest({ isActive: true }), params()) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts index 9b6f523c5fd..d056dfa0223 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts @@ -7,7 +7,11 @@ import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getShareForResource, upsertFileShare } from '@/lib/public-shares/share-manager' +import { + getShareForResource, + ShareValidationError, + upsertFileShare, +} from '@/lib/public-shares/share-manager' import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { @@ -81,7 +85,7 @@ export const PUT = withRouteHandler( const parsed = await parseRequest(upsertFileShareContract, request, context) if (!parsed.success) return parsed.response const { id: workspaceId, fileId } = parsed.data.params - const { isActive } = parsed.data.body + const { isActive, authType, password, allowedEmails, token } = parsed.data.body const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) if (permission !== 'admin' && permission !== 'write') { @@ -96,11 +100,12 @@ export const PUT = withRouteHandler( return NextResponse.json({ error: 'File not found' }, { status: 404 }) } - // Enabling a public link is gated by the org's access-control policy; disabling - // is always allowed so users can still un-share after the policy is turned on. + // Enabling a share is gated by the org's access-control policy (both the + // master on/off and the per-auth-type allow-list); disabling is always + // allowed so users can still un-share after the policy is turned on. if (isActive) { try { - await validatePublicFileSharing(session.user.id, workspaceId) + await validatePublicFileSharing(session.user.id, workspaceId, authType ?? 'public') } catch (error) { if (error instanceof PublicFileSharingNotAllowedError) { logger.warn(`[${requestId}] Public file sharing disabled for workspace ${workspaceId}`) @@ -115,6 +120,10 @@ export const PUT = withRouteHandler( fileId, userId: session.user.id, isActive, + authType, + password, + allowedEmails, + token, }) logger.info(`[${requestId}] ${isActive ? 'Enabled' : 'Disabled'} share for file ${fileId}`) @@ -134,6 +143,9 @@ export const PUT = withRouteHandler( return NextResponse.json({ share }) } catch (error) { + if (error instanceof ShareValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } logger.error(`[${requestId}] Error updating file share:`, error) return NextResponse.json( { error: getErrorMessage(error, 'Failed to update share') }, diff --git a/apps/sim/app/f/[token]/opengraph-image.tsx b/apps/sim/app/f/[token]/opengraph-image.tsx new file mode 100644 index 00000000000..b5f6541ba3c --- /dev/null +++ b/apps/sim/app/f/[token]/opengraph-image.tsx @@ -0,0 +1,37 @@ +import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' +import { createLandingOgImage } from '@/app/(landing)/og-utils' +import { buildProvenance } from '@/app/f/[token]/utils' + +export const dynamic = 'force-dynamic' +export const contentType = 'image/png' +export const size = { + width: 1200, + height: 630, +} + +/** + * Social-preview card for a shared file. Public shares show the file name + + * provenance; protected (password / email / SSO) and unknown shares stay generic + * so the filename never leaks pre-auth. + */ +export default async function Image({ params }: { params: Promise<{ token: string }> }) { + const { token } = await params + const resolved = await resolveActiveShareByToken(token) + + if (!resolved || resolved.share.authType !== 'public') { + return createLandingOgImage({ + eyebrow: 'Shared file', + title: 'Protected file', + subtitle: 'Authentication is required to view this file', + }) + } + + const { file, workspaceName, ownerName } = resolved + const subtitle = buildProvenance(workspaceName, ownerName) || 'Shared via Sim' + + return createLandingOgImage({ + eyebrow: 'Shared file', + title: file.originalName, + subtitle, + }) +} diff --git a/apps/sim/app/f/[token]/page.tsx b/apps/sim/app/f/[token]/page.tsx index 2e75f2f7989..3d5506b6919 100644 --- a/apps/sim/app/f/[token]/page.tsx +++ b/apps/sim/app/f/[token]/page.tsx @@ -1,28 +1,116 @@ +import { cache } from 'react' import type { Metadata } from 'next' +import { cookies } from 'next/headers' import { notFound } from 'next/navigation' +import { getSession } from '@/lib/auth' +import { + deploymentAuthCookieName, + isEmailAllowed, + validateAuthToken, +} from '@/lib/core/security/deployment' import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager' +import { PublicFileAuth } from '@/app/f/[token]/public-file-auth' +import { PublicFileEmailAuth } from '@/app/f/[token]/public-file-email-auth' +import { PublicFileSSOAuth } from '@/app/f/[token]/public-file-sso-auth' import { PublicFileView } from '@/app/f/[token]/public-file-view' +import { buildProvenance } from '@/app/f/[token]/utils' +import { getBrandConfig } from '@/ee/whitelabeling' export const dynamic = 'force-dynamic' +/** Deduped per-request so `generateMetadata` and the page share one DB resolve. */ +const resolveShare = cache(resolveActiveShareByToken) + /** Shared links must never be indexed by search engines. */ -export const metadata: Metadata = { - robots: { index: false, follow: false }, -} +const NOINDEX = { index: false, follow: false } as const interface PublicFilePageProps { params: Promise<{ token: string }> } +/** + * Social-preview metadata. Public shares unfurl with the file name + provenance; + * any protected share (password / email / SSO) stays deliberately generic so the + * filename never leaks before the visitor authenticates. Always `noindex`. + */ +export async function generateMetadata({ params }: PublicFilePageProps): Promise { + const { token } = await params + const resolved = await resolveShare(token) + if (!resolved) { + return { robots: NOINDEX } + } + + let title: string + let description: string + if (resolved.share.authType !== 'public') { + title = 'Shared file' + description = 'Authentication is required to view this file.' + } else { + title = resolved.file.originalName + description = + buildProvenance(resolved.workspaceName, resolved.ownerName) || `Shared file · ${title}` + } + + const brand = getBrandConfig() + return { + title, + description, + robots: NOINDEX, + openGraph: { type: 'website', title, description, siteName: brand.name }, + twitter: { card: 'summary_large_image', title, description }, + } +} + +/** The auth-relevant slice of a resolved share row. */ +interface GateShare { + id: string + authType: string + password: string | null + allowedEmails: unknown +} + +/** + * Returns the auth prompt to render when a protected share is not yet authorized, + * or `null` when the visitor may view the file. `password`/`email` use the + * `file_auth_{shareId}` cookie; `sso` uses the global Sim session. + */ +async function renderAuthGate(token: string, share: GateShare) { + if (share.authType === 'public') return null + + if (share.authType === 'sso') { + const session = await getSession() + const allowedEmails = Array.isArray(share.allowedEmails) + ? (share.allowedEmails as string[]) + : [] + const authorized = Boolean( + session?.user?.email && isEmailAllowed(session.user.email, allowedEmails) + ) + return authorized ? null : + } + + const cookieStore = await cookies() + const cookieValue = cookieStore.get(deploymentAuthCookieName('file', share.id))?.value + if (validateAuthToken(cookieValue ?? '', share.id, share.authType, share.password)) return null + + return share.authType === 'email' ? ( + + ) : ( + + ) +} + export default async function PublicFilePage({ params }: PublicFilePageProps) { const { token } = await params - const resolved = await resolveActiveShareByToken(token) + const resolved = await resolveShare(token) if (!resolved) { notFound() } - const { file, workspaceName, ownerName } = resolved + const { share, file, workspaceName, ownerName } = resolved + + const gate = await renderAuthGate(token, share) + if (gate) return gate return ( +
+
+ +
+
+
+
+
+

+ {title} +

+

+ {subtitle} +

+
+
{children}
+
+
+
+ +
+ + ) +} diff --git a/apps/sim/app/f/[token]/public-file-auth.tsx b/apps/sim/app/f/[token]/public-file-auth.tsx new file mode 100644 index 00000000000..fa7d57dd05a --- /dev/null +++ b/apps/sim/app/f/[token]/public-file-auth.tsx @@ -0,0 +1,102 @@ +'use client' + +import { useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { Eye, EyeOff } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { Input, Label, Loader } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' +import { PublicFileAuthShell } from '@/app/f/[token]/public-file-auth-shell' +import { usePublicFileAuth } from '@/hooks/queries/public-shares' + +interface PublicFileAuthProps { + token: string +} + +/** + * Password gate for a protected public file share. On success the + * `file_auth_{shareId}` cookie is set and the page re-renders the viewer. + */ +export function PublicFileAuth({ token }: PublicFileAuthProps) { + const router = useRouter() + const authenticate = usePublicFileAuth(token) + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [error, setError] = useState(null) + + const handleAuthenticate = async () => { + if (!password.trim()) { + setError('Password is required.') + return + } + setError(null) + try { + await authenticate.mutateAsync({ password }) + router.refresh() + } catch (err) { + setError(getErrorMessage(err, 'Invalid password. Please try again.')) + } + } + + return ( + +
{ + e.preventDefault() + handleAuthenticate() + }} + className='space-y-6' + > +
+ +
+ { + setPassword(e.target.value) + setError(null) + }} + className={cn( + 'pr-10', + error && 'border-[var(--text-error)] focus:border-[var(--text-error)]' + )} + /> + +
+ {error ?

{error}

: null} +
+ + +
+
+ ) +} diff --git a/apps/sim/app/f/[token]/public-file-email-auth.tsx b/apps/sim/app/f/[token]/public-file-email-auth.tsx new file mode 100644 index 00000000000..b05b487574c --- /dev/null +++ b/apps/sim/app/f/[token]/public-file-email-auth.tsx @@ -0,0 +1,217 @@ +'use client' + +import { useEffect, useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { useRouter } from 'next/navigation' +import { Input, InputOTP, InputOTPGroup, InputOTPSlot, Label, Loader } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { quickValidateEmail } from '@/lib/messaging/email/validation' +import { AUTH_SUBMIT_BTN, AUTH_TEXT_LINK } from '@/app/(auth)/components/auth-button-classes' +import { PublicFileAuthShell } from '@/app/f/[token]/public-file-auth-shell' +import { usePublicFileOtpRequest, usePublicFileOtpVerify } from '@/hooks/queries/public-shares' + +interface PublicFileEmailAuthProps { + token: string +} + +/** + * Email-OTP gate for a protected public file share: collect an allow-listed email, + * send a 6-digit code, verify it. On success the server sets the + * `file_auth_{shareId}` cookie and the page re-renders the viewer. + */ +export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { + const router = useRouter() + const requestOtp = usePublicFileOtpRequest(token) + const verifyOtp = usePublicFileOtpVerify(token) + + const [email, setEmail] = useState('') + const [otp, setOtp] = useState('') + const [sent, setSent] = useState(false) + const [error, setError] = useState(null) + const [countdown, setCountdown] = useState(0) + + useEffect(() => { + if (countdown <= 0) return + const timer = setTimeout(() => setCountdown((c) => c - 1), 1000) + return () => clearTimeout(timer) + }, [countdown]) + + const sendCode = async () => { + if (!quickValidateEmail(email.trim().toLowerCase()).isValid) { + setError('Please enter a valid email address.') + return + } + setError(null) + try { + await requestOtp.mutateAsync({ email: email.trim().toLowerCase() }) + setSent(true) + setOtp('') + } catch (err) { + setError(getErrorMessage(err, 'Failed to send verification code')) + } + } + + const verifyCode = async (code: string) => { + if (code.length !== 6) return + setError(null) + try { + await verifyOtp.mutateAsync({ email: email.trim().toLowerCase(), otp: code }) + router.refresh() + } catch (err) { + setError(getErrorMessage(err, 'Invalid verification code')) + } + } + + const resend = async () => { + setCountdown(30) + try { + await requestOtp.mutateAsync({ email: email.trim().toLowerCase() }) + setOtp('') + setError(null) + } catch (err) { + setCountdown(0) + setError(getErrorMessage(err, 'Failed to resend verification code')) + } + } + + if (!sent) { + return ( + +
{ + e.preventDefault() + sendCode() + }} + className='space-y-6' + > +
+ + { + setEmail(e.target.value) + setError(null) + }} + className={cn(error && 'border-[var(--text-error)] focus:border-[var(--text-error)]')} + /> + {error ?

{error}

: null} +
+ + +
+
+ ) + } + + return ( + +
+

+ Enter the 6-digit code to verify your access. If you don't see it in your inbox, check + your spam folder. +

+ +
+ { + setOtp(value) + setError(null) + if (value.length === 6) verifyCode(value) + }} + disabled={verifyOtp.isPending} + className={cn('gap-2', error && 'otp-error')} + > + + {[0, 1, 2, 3, 4, 5].map((i) => ( + + ))} + + +
+ + {error ?

{error}

: null} + + + +
+

+ Didn't receive a code?{' '} + {countdown > 0 ? ( + + Resend in{' '} + {countdown}s + + ) : ( + + )} +

+
+ +
+ +
+
+
+ ) +} diff --git a/apps/sim/app/f/[token]/public-file-sso-auth.tsx b/apps/sim/app/f/[token]/public-file-sso-auth.tsx new file mode 100644 index 00000000000..247975a4b29 --- /dev/null +++ b/apps/sim/app/f/[token]/public-file-sso-auth.tsx @@ -0,0 +1,103 @@ +'use client' + +import { useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { useRouter } from 'next/navigation' +import { Input, Label, Loader } from '@/components/emcn' +import { requestJson } from '@/lib/api/client/request' +import { publicFileSSOContract } from '@/lib/api/contracts/public-shares' +import { cn } from '@/lib/core/utils/cn' +import { quickValidateEmail } from '@/lib/messaging/email/validation' +import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' +import { PublicFileAuthShell } from '@/app/f/[token]/public-file-auth-shell' + +interface PublicFileSSOAuthProps { + token: string +} + +/** + * SSO gate for a protected public file share: confirm the email is allow-listed, + * then hand off to the global `/sso` flow with this share as the callback. After + * sign-in the page gate authorizes via the Sim session. + */ +export function PublicFileSSOAuth({ token }: PublicFileSSOAuthProps) { + const router = useRouter() + const [email, setEmail] = useState('') + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + + const handleAuthenticate = async () => { + if (!quickValidateEmail(email.trim().toLowerCase()).isValid) { + setError('Please enter a valid email address.') + return + } + setError(null) + setIsLoading(true) + try { + const normalizedEmail = email.trim().toLowerCase() + const { eligible } = await requestJson(publicFileSSOContract, { + params: { token }, + body: { email: normalizedEmail }, + }) + if (!eligible) { + setError('Email not authorized for this file.') + setIsLoading(false) + return + } + const callbackUrl = `/f/${token}` + router.push( + `/sso?email=${encodeURIComponent(normalizedEmail)}&callbackUrl=${encodeURIComponent(callbackUrl)}` + ) + } catch (err) { + setError(getErrorMessage(err, 'Email not authorized for this file.')) + setIsLoading(false) + } + } + + return ( + +
{ + e.preventDefault() + handleAuthenticate() + }} + className='space-y-6' + > +
+ + { + setEmail(e.target.value) + setError(null) + }} + className={cn(error && 'border-[var(--text-error)] focus:border-[var(--text-error)]')} + /> + {error ?

{error}

: null} +
+ + +
+
+ ) +} diff --git a/apps/sim/app/f/[token]/public-file-view.tsx b/apps/sim/app/f/[token]/public-file-view.tsx index 03ea35d795e..f27b63df65d 100644 --- a/apps/sim/app/f/[token]/public-file-view.tsx +++ b/apps/sim/app/f/[token]/public-file-view.tsx @@ -6,6 +6,7 @@ import Link from 'next/link' import { Chip } from '@/components/emcn' import { Download } from '@/components/emcn/icons' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import { buildProvenance } from '@/app/f/[token]/utils' import { FileViewer } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { useBrandConfig } from '@/ee/whitelabeling' import { type FileContentSource, FileContentSourceProvider } from '@/hooks/use-file-content-source' @@ -32,9 +33,7 @@ export function PublicFileView({ }: PublicFileViewProps) { const contentUrl = `/api/files/public/${token}/content` const brand = useBrandConfig() - const provenance = [workspaceName, ownerName ? `Shared by ${ownerName}` : null] - .filter(Boolean) - .join(' · ') + const provenance = buildProvenance(workspaceName, ownerName) // The public viewer reuses the in-app FileViewer; the content source seam swaps // the auth-gated workspace serve URL for the token-scoped public endpoint, and a diff --git a/apps/sim/app/f/[token]/utils.ts b/apps/sim/app/f/[token]/utils.ts new file mode 100644 index 00000000000..0dcf8983ca5 --- /dev/null +++ b/apps/sim/app/f/[token]/utils.ts @@ -0,0 +1,9 @@ +/** + * Provenance label for a shared file (`"{workspace} · Shared by {owner}"`), shared + * by the page metadata, the OG card, and the in-page viewer so the three never + * drift. Returns an empty string when neither is known; callers apply their own + * fallback. + */ +export function buildProvenance(workspaceName: string | null, ownerName: string | null): string { + return [workspaceName, ownerName ? `Shared by ${ownerName}` : null].filter(Boolean).join(' · ') +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx index 57596a8aa3f..f2e4326b132 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx @@ -1,16 +1,24 @@ 'use client' import { useState } from 'react' +import { generateShortId } from '@sim/utils/id' import { + ButtonGroup, + ButtonGroupItem, ChipModal, ChipModalBody, ChipModalField, ChipModalFooter, ChipModalHeader, - ChipSwitch, + TagInput, + type TagItem, } from '@/components/emcn' import { Link } from '@/components/emcn/icons' -import type { ShareRecord } from '@/lib/api/contracts/public-shares' +import { GeneratedPasswordInput } from '@/components/ui' +import type { ShareAuthType, ShareRecord } from '@/lib/api/contracts/public-shares' +import { getEnv, isTruthy } from '@/lib/core/config/env' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { quickValidateEmail } from '@/lib/messaging/email/validation' import { useFileShare, useUpsertFileShare } from '@/hooks/queries/public-shares' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -24,10 +32,26 @@ interface ShareModalProps { initialShare?: ShareRecord | null } -const VISIBILITY_OPTIONS = [ - { value: 'private', label: 'Private' }, - { value: 'public', label: 'Anyone with link' }, -] +type AccessMode = 'private' | ShareAuthType + +const ACCESS_LABELS: Record = { + private: 'Private', + public: 'Public', + password: 'Password', + email: 'Email', + sso: 'SSO', +} + +function savedMode(share: ShareRecord | null): AccessMode { + if (!share?.isActive) return 'private' + return share.authType +} + +/** True when an entry is a valid email or an `@domain` pattern. */ +function isValidEmailEntry(value: string): boolean { + const normalized = value.trim().toLowerCase() + return normalized.startsWith('@') || quickValidateEmail(normalized).isValid +} export function ShareModal({ open, @@ -37,67 +61,209 @@ export function ShareModal({ fileName, initialShare, }: ShareModalProps) { - const { data: share } = useFileShare(workspaceId, fileId, { enabled: open }) + const { data: share, isFetched } = useFileShare(workspaceId, fileId, { enabled: open }) const { config: permissionConfig } = usePermissionConfig() const upsertShare = useUpsertFileShare() const saved = share ?? initialShare ?? null - const savedActive = saved?.isActive ?? false + const savedAccessMode = savedMode(saved) + + // Reserve a token on open (one per mount — the modal remounts each open) so the + // link can be shown and copied before the first save; it's persisted on save. + // Only used once we've confirmed no share row exists yet, so a copied link + // always matches what gets stored. + const [pendingToken] = useState(() => generateShortId()) + const noExistingShare = isFetched && !share && !initialShare + const shareUrl = saved?.url ?? (noExistingShare ? `${getBaseUrl()}/f/${pendingToken}` : null) + + // `null` until the user changes the selector, so the control always reflects the + // authoritative saved state (which may resolve after mount via useFileShare). + const [draftMode, setDraftMode] = useState(null) + const [draftPassword, setDraftPassword] = useState('') + const [draftEmails, setDraftEmails] = useState(null) + const effectiveMode = draftMode ?? savedAccessMode + const effectiveActive = effectiveMode !== 'private' + const effectiveEmails = draftEmails ?? saved?.allowedEmails ?? [] + + // Org access-control may restrict which auth modes are allowed (`null` = all). + // The route is the source of truth; this just hides disallowed options. + const allowedAuthTypes = permissionConfig.allowedFileShareAuthTypes + const isAuthTypeAllowed = (mode: ShareAuthType) => + allowedAuthTypes === null || allowedAuthTypes.includes(mode) + + const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) || savedAccessMode === 'sso' + const candidateAuthTypes: ShareAuthType[] = [ + 'public', + 'password', + 'email', + ...(ssoEnabled ? (['sso'] as const) : []), + ] + // Keep the saved mode visible even if newly disallowed, so the current state shows. + const accessModes: AccessMode[] = [ + 'private', + ...candidateAuthTypes.filter((mode) => isAuthTypeAllowed(mode) || mode === savedAccessMode), + ] + + // The selected mode is blocked when org policy disables public sharing entirely + // (enabling a new share) or when the chosen auth mode isn't allowed. + const modeDisallowed = effectiveMode !== 'private' && !isAuthTypeAllowed(effectiveMode) + const enableBlockedByPolicy = + (permissionConfig.disablePublicFileSharing && !saved?.isActive) || modeDisallowed + + // A password share needs a secret: either one already stored or a freshly typed one. + const passwordMissing = + effectiveMode === 'password' && !saved?.hasPassword && draftPassword.trim().length === 0 + // Email/SSO shares need at least one allowed email/domain. + const emailsMissing = + (effectiveMode === 'email' || effectiveMode === 'sso') && effectiveEmails.length === 0 - // Org access-control policy can disable enabling new public links (the route is the - // source of truth; this just reflects it). Disabling an existing share stays allowed. - const enableBlockedByPolicy = permissionConfig.disablePublicFileSharing && !savedActive + const emailsDirty = + draftEmails !== null && + JSON.stringify(draftEmails) !== JSON.stringify(saved?.allowedEmails ?? []) + const isDirty = + (draftMode !== null && draftMode !== savedAccessMode) || + (effectiveMode === 'password' && draftPassword.length > 0) || + ((effectiveMode === 'email' || effectiveMode === 'sso') && emailsDirty) - // `null` until the user toggles, so the switch always reflects the authoritative - // saved state (which may resolve after mount via useFileShare) instead of a stale - // initial snapshot — otherwise a Save could silently flip sharing the wrong way. - const [draftActive, setDraftActive] = useState(null) - const effectiveActive = draftActive ?? savedActive - const isDirty = draftActive !== null && draftActive !== savedActive + const resetDraft = () => { + setDraftMode(null) + setDraftPassword('') + setDraftEmails(null) + } + + const handleClose = () => { + resetDraft() + onOpenChange(false) + } const handleSave = () => { - upsertShare.mutate( - { workspaceId, fileId, isActive: effectiveActive }, - { onSuccess: () => onOpenChange(false) } - ) + // Persist the reserved token only when creating the row; existing shares keep + // their own token (the server ignores this on conflict). + const base = { workspaceId, fileId, token: saved ? undefined : pendingToken } + const vars = + effectiveMode === 'private' + ? { ...base, isActive: false as const } + : effectiveMode === 'password' + ? { + ...base, + isActive: true as const, + authType: 'password' as const, + password: draftPassword.trim() || undefined, + } + : effectiveMode === 'email' || effectiveMode === 'sso' + ? { + ...base, + isActive: true as const, + authType: effectiveMode, + allowedEmails: effectiveEmails, + } + : { ...base, isActive: true as const, authType: 'public' as const } + + upsertShare.mutate(vars, { + onSuccess: () => { + resetDraft() + onOpenChange(false) + }, + }) + } + + const addEmail = (value: string): boolean => { + const normalized = value.trim().toLowerCase() + if (!normalized || effectiveEmails.includes(normalized) || !isValidEmailEntry(normalized)) { + return false + } + setDraftEmails([...effectiveEmails, normalized]) + return true } + const removeEmail = (_value: string, index: number) => { + setDraftEmails(effectiveEmails.filter((_, i) => i !== index)) + } + + const accessHint = (() => { + if (modeDisallowed) return 'This sharing method is disabled by an administrator.' + if (enableBlockedByPolicy) + return 'Public sharing is disabled for this workspace by an administrator.' + if (effectiveMode === 'private') return 'Only workspace members can access this file.' + if (effectiveMode === 'password') + return 'Anyone with the link and the password can view and download this file.' + if (effectiveMode === 'email') + return 'Only allowed emails can access this file after a one-time code.' + if (effectiveMode === 'sso') + return 'Only allowed emails signed in via SSO can access this file.' + return isDirty + ? 'Save to make this file accessible to anyone with the link.' + : 'Anyone with the link can view and download this file.' + })() + + const emailItems: TagItem[] = effectiveEmails.map((value) => ({ value, isValid: true })) + return ( - - onOpenChange(false)}> + + Share file - - setDraftActive(value === 'public')} - options={VISIBILITY_OPTIONS} + + setDraftMode(value as AccessMode)} aria-label='File access' - /> + > + {accessModes.map((mode) => ( + + {ACCESS_LABELS[mode]} + + ))} + - {saved?.isActive ? ( - + {effectiveMode === 'password' ? ( + + + + ) : null} + {effectiveMode === 'email' || effectiveMode === 'sso' ? ( + + + + ) : null} + {effectiveMode !== 'private' && shareUrl ? ( + ) : null} onOpenChange(false)} + onCancel={handleClose} primaryAction={{ label: upsertShare.isPending ? 'Saving...' : 'Save', onClick: handleSave, - disabled: !isDirty || upsertShare.isPending || (effectiveActive && enableBlockedByPolicy), + disabled: + !isDirty || + upsertShare.isPending || + passwordMissing || + emailsMissing || + (effectiveActive && enableBlockedByPolicy), }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index 8f6218731e0..12fe7164ce6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -3,9 +3,8 @@ import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { AlertTriangle, Check, Clipboard, Eye, EyeOff, RefreshCw } from 'lucide-react' +import { AlertTriangle, Check } from 'lucide-react' import { - Button, ButtonGroup, ButtonGroupItem, ChipConfirmModal, @@ -19,8 +18,8 @@ import { Textarea, Tooltip, } from '@/components/emcn' +import { GeneratedPasswordInput } from '@/components/ui' import { getEnv, isTruthy } from '@/lib/core/config/env' -import { generatePassword } from '@/lib/core/security/encryption' import { cn } from '@/lib/core/utils/cn' import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls' import { quickValidateEmail } from '@/lib/messaging/email/validation' @@ -611,9 +610,7 @@ function AuthSelector({ hasExistingPassword = false, error, }: AuthSelectorProps) { - const [showPassword, setShowPassword] = useState(false) const [emailError, setEmailError] = useState('') - const [copySuccess, setCopySuccess] = useState(false) const [invalidEmailItems, setInvalidEmailItems] = useState([]) const emailsRef = useRef(emails) @@ -623,22 +620,6 @@ function AuthSelector({ emailsRef.current = emails }, [emails]) - useEffect(() => { - if (!copySuccess) return - const timer = setTimeout(() => setCopySuccess(false), 2000) - return () => clearTimeout(timer) - }, [copySuccess]) - - const handleGeneratePassword = () => { - const newPassword = generatePassword(24) - onPasswordChange(newPassword) - } - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - setCopySuccess(true) - } - const addEmail = (email: string): boolean => { if (!email.trim()) return false @@ -718,73 +699,12 @@ function AuthSelector({ - onPasswordChange(e.target.value)} + onChange={onPasswordChange} disabled={disabled} + placeholder={getPasswordPlaceholder(hasExistingPassword)} required={!hasExistingPassword} - autoComplete='new-password' - endAdornment={ -
- - - - - - Generate - - - - - - - - {copySuccess ? 'Copied' : 'Copy'} - - - - - - - - {showPassword ? 'Hide' : 'Show'} - - -
- } />

{getPasswordHelperText(hasExistingPassword)} diff --git a/apps/sim/components/ui/generated-password-input.tsx b/apps/sim/components/ui/generated-password-input.tsx new file mode 100644 index 00000000000..7cee9884fa1 --- /dev/null +++ b/apps/sim/components/ui/generated-password-input.tsx @@ -0,0 +1,118 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Check, Clipboard, Eye, EyeOff, RefreshCw } from 'lucide-react' +import { Button, ChipInput, Tooltip } from '@/components/emcn' +import { generatePassword } from '@/lib/core/security/encryption' + +interface GeneratedPasswordInputProps { + value: string + onChange: (value: string) => void + disabled?: boolean + placeholder?: string + /** Show the Generate (random password) action. Off for consumer-facing entry forms. */ + showGenerate?: boolean + required?: boolean + autoComplete?: string + error?: boolean +} + +/** + * Password field with reveal / copy / (optional) generate adornments, used by the + * deploy-as-chat access controls and the file-share modal. Owns its show/copy UI + * state; the caller owns the value. + */ +export function GeneratedPasswordInput({ + value, + onChange, + disabled = false, + placeholder, + showGenerate = true, + required = false, + autoComplete = 'new-password', + error = false, +}: GeneratedPasswordInputProps) { + const [showPassword, setShowPassword] = useState(false) + const [copySuccess, setCopySuccess] = useState(false) + + useEffect(() => { + if (!copySuccess) return + const timer = setTimeout(() => setCopySuccess(false), 2000) + return () => clearTimeout(timer) + }, [copySuccess]) + + const copyToClipboard = () => { + navigator.clipboard.writeText(value) + setCopySuccess(true) + } + + return ( + onChange(e.target.value)} + disabled={disabled} + required={required} + autoComplete={autoComplete} + error={error} + endAdornment={ +

+ {showGenerate ? ( + + + + + + Generate + + + ) : null} + + + + + + {copySuccess ? 'Copied' : 'Copy'} + + + + + + + + {showPassword ? 'Hide' : 'Show'} + + +
+ } + /> + ) +} diff --git a/apps/sim/components/ui/index.ts b/apps/sim/components/ui/index.ts index 2ea97b0b4ae..dfd0c105fb1 100644 --- a/apps/sim/components/ui/index.ts +++ b/apps/sim/components/ui/index.ts @@ -1,4 +1,5 @@ export { Button, buttonVariants } from './button' +export { GeneratedPasswordInput } from './generated-password-input' export { Progress } from './progress' export { SearchHighlight } from './search-highlight' export { diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 0bf7de4dc47..fd3b19e5799 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -35,6 +35,7 @@ import { toast, } from '@/components/emcn' import { ArrowLeft } from '@/components/emcn/icons' +import type { ShareAuthType } from '@/lib/api/contracts/public-shares' import { getEnv, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access' @@ -69,6 +70,15 @@ import type { ProviderName } from '@/stores/providers' const logger = createLogger('AccessControl') +/** Public-file-share auth modes an admin can allow/disallow. `null` config = all allowed. */ +const FILE_SHARE_AUTH_TYPE_OPTIONS: { value: ShareAuthType; label: string }[] = [ + { value: 'public', label: 'Anyone with link' }, + { value: 'password', label: 'Password' }, + { value: 'email', label: 'Email' }, + { value: 'sso', label: 'SSO' }, +] +const ALL_FILE_SHARE_AUTH_TYPES: ShareAuthType[] = FILE_SHARE_AUTH_TYPE_OPTIONS.map((o) => o.value) + interface OrganizationMemberOption { userId: string user: { @@ -1070,6 +1080,36 @@ export function AccessControl() { [editingConfig, allProviderIds] ) + const isFileShareAuthAllowed = useCallback( + (authType: ShareAuthType) => { + if (!editingConfig) return true + return ( + editingConfig.allowedFileShareAuthTypes === null || + editingConfig.allowedFileShareAuthTypes.includes(authType) + ) + }, + [editingConfig] + ) + + const toggleFileShareAuthType = useCallback( + (authType: ShareAuthType) => { + if (!editingConfig) return + const current = editingConfig.allowedFileShareAuthTypes + const next = + current === null + ? ALL_FILE_SHARE_AUTH_TYPES.filter((t) => t !== authType) + : current.includes(authType) + ? current.filter((t) => t !== authType) + : [...current, authType] + // A full list collapses back to `null` ("all allowed"). + setEditingConfig({ + ...editingConfig, + allowedFileShareAuthTypes: next.length === ALL_FILE_SHARE_AUTH_TYPES.length ? null : next, + }) + }, + [editingConfig] + ) + const isIntegrationAllowed = useCallback( (blockType: string) => { if (!editingConfig) return true @@ -1603,6 +1643,30 @@ export function AccessControl() {
))}
+
+ + File Sharing Methods + +

+ Auth modes that public file-share links may use. +

+
+ {FILE_SHARE_AUTH_TYPE_OPTIONS.map(({ value, label }) => ( + + ))} +
+
)} diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index e6fcfada0cc..6821d9fae83 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -33,6 +33,7 @@ const { disableInvitations: false, disablePublicApi: false, disablePublicFileSharing: false, + allowedFileShareAuthTypes: null, hideDeployApi: false, hideDeployMcp: false, hideDeployA2a: false, @@ -149,10 +150,12 @@ import { McpToolsNotAllowedError, ModelNotAllowedError, ProviderNotAllowedError, + PublicFileSharingNotAllowedError, SkillsNotAllowedError, validateBlockType, validateMcpToolsAllowed, validateModelProvider, + validatePublicFileSharing, } from './permission-check' /** Default an org-backed, enterprise-entitled workspace so resolution reaches the group queries. */ @@ -532,6 +535,46 @@ describe('validateMcpToolsAllowed', () => { }) }) +describe('validatePublicFileSharing', () => { + beforeEach(() => { + vi.clearAllMocks() + mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] + mockDefaultGroup.value = [] + mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) + setEnterpriseOrgWorkspace() + }) + + it('throws when public file sharing is fully disabled', async () => { + mockExplicitGroup.value = [{ config: { disablePublicFileSharing: true } }] + await expect( + validatePublicFileSharing('user-123', 'workspace-1', 'password') + ).rejects.toBeInstanceOf(PublicFileSharingNotAllowedError) + }) + + it('throws when the auth type is not in the allow-list', async () => { + mockExplicitGroup.value = [{ config: { allowedFileShareAuthTypes: ['password', 'sso'] } }] + await expect( + validatePublicFileSharing('user-123', 'workspace-1', 'public') + ).rejects.toBeInstanceOf(PublicFileSharingNotAllowedError) + }) + + it('allows an auth type that is in the allow-list', async () => { + mockExplicitGroup.value = [{ config: { allowedFileShareAuthTypes: ['password', 'sso'] } }] + await validatePublicFileSharing('user-123', 'workspace-1', 'password') + }) + + it('allows any auth type when the allow-list is null', async () => { + mockExplicitGroup.value = [{ config: { allowedFileShareAuthTypes: null } }] + await validatePublicFileSharing('user-123', 'workspace-1', 'email') + }) + + it('no-ops when no auth type is provided (master switch only)', async () => { + mockExplicitGroup.value = [{ config: { allowedFileShareAuthTypes: ['password'] } }] + await validatePublicFileSharing('user-123', 'workspace-1') + }) +}) + describe('assertPermissionsAllowed', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/ee/access-control/utils/permission-check.ts b/apps/sim/ee/access-control/utils/permission-check.ts index e493537456b..025618ad7bd 100644 --- a/apps/sim/ee/access-control/utils/permission-check.ts +++ b/apps/sim/ee/access-control/utils/permission-check.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { permissionGroup, permissionGroupMember, permissionGroupWorkspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, asc, eq } from 'drizzle-orm' +import type { ShareAuthType } from '@/lib/api/contracts/public-shares' import { isOrganizationOnEnterprisePlan } from '@/lib/billing' import { getAllowedIntegrationsFromEnv, @@ -293,15 +294,33 @@ export async function getUserPermissionConfig( /** * Throws {@link PublicFileSharingNotAllowedError} if the user's effective permission - * group for the workspace disables public file sharing. No-op when access control - * doesn't apply (non-enterprise / disabled), so non-governed orgs are unaffected. + * group for the workspace disables public file sharing, or — when `authType` is + * given — if that auth mode isn't in the group's `allowedFileShareAuthTypes` + * allow-list (`null` allows all). No-op when access control doesn't apply + * (non-enterprise / disabled), so non-governed orgs are unaffected. */ export async function validatePublicFileSharing( userId: string, - workspaceId: string + workspaceId: string, + authType?: ShareAuthType ): Promise { const config = await getUserPermissionConfig(userId, workspaceId) - if (config?.disablePublicFileSharing) { + if (!config) { + return + } + if (config.disablePublicFileSharing) { + throw new PublicFileSharingNotAllowedError() + } + if ( + authType && + config.allowedFileShareAuthTypes !== null && + !config.allowedFileShareAuthTypes.includes(authType) + ) { + logger.warn('File share auth type blocked by permission group', { + userId, + workspaceId, + authType, + }) throw new PublicFileSharingNotAllowedError() } } diff --git a/apps/sim/hooks/queries/public-shares.ts b/apps/sim/hooks/queries/public-shares.ts index a32cf0a968f..08382e638cd 100644 --- a/apps/sim/hooks/queries/public-shares.ts +++ b/apps/sim/hooks/queries/public-shares.ts @@ -2,10 +2,15 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from '@/components/emcn' import { requestJson } from '@/lib/api/client/request' import { + type AuthenticatePublicFileResponse, + authenticatePublicFileContract, getFileShareContract, + requestPublicFileOtpContract, type ShareRecord, type UpsertFileShareBody, upsertFileShareContract, + type VerifyPublicFileOtpResponse, + verifyPublicFileOtpContract, } from '@/lib/api/contracts/public-shares' import { workspaceFilesKeys } from '@/hooks/queries/workspace-files' @@ -48,34 +53,57 @@ interface UpsertFileShareVariables extends UpsertFileShareBody { export function useUpsertFileShare() { const queryClient = useQueryClient() return useMutation({ - mutationFn: ({ workspaceId, fileId, isActive }: UpsertFileShareVariables) => + mutationFn: ({ workspaceId, fileId, ...body }: UpsertFileShareVariables) => requestJson(upsertFileShareContract, { params: { id: workspaceId, fileId }, - body: { isActive }, + body, }), - onSuccess: (data, { workspaceId, fileId, isActive }) => { + onSuccess: (data, { workspaceId, fileId }) => { queryClient.setQueryData(shareKeys.detail(workspaceId, fileId), data.share) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.workspaceLists(workspaceId) }) - if (!isActive) { - toast.success('Sharing turned off') - return - } - const { url } = data.share - toast.success('Public link enabled', { - description: url, - action: { - label: 'Copy link', - onClick: () => { - navigator.clipboard.writeText(url).then( - () => toast.success('Link copied'), - () => toast.error('Failed to copy link') - ) - }, - }, - }) }, onError: (error) => { toast.error(error.message) }, }) } + +/** + * Exchanges a share password for a `file_auth_{shareId}` cookie on the public + * file page. On success the page should `router.refresh()` to re-render the + * now-authorized viewer. + */ +export function usePublicFileAuth(token: string) { + return useMutation({ + mutationFn: ({ password }) => + requestJson(authenticatePublicFileContract, { + params: { token }, + body: { password }, + }), + }) +} + +/** Requests a verification code for an email-gated share (initial send + resend). */ +export function usePublicFileOtpRequest(token: string) { + return useMutation<{ message: string }, Error, { email: string }>({ + mutationFn: ({ email }) => + requestJson(requestPublicFileOtpContract, { + params: { token }, + body: { email }, + }), + }) +} + +/** + * Verifies the OTP for an email-gated share. On success the server sets the + * `file_auth_{shareId}` cookie; the page should then `router.refresh()`. + */ +export function usePublicFileOtpVerify(token: string) { + return useMutation({ + mutationFn: ({ email, otp }) => + requestJson(verifyPublicFileOtpContract, { + params: { token }, + body: { email, otp }, + }), + }) +} diff --git a/apps/sim/lib/api/contracts/permission-groups.ts b/apps/sim/lib/api/contracts/permission-groups.ts index 950307ed915..0f4b24ba015 100644 --- a/apps/sim/lib/api/contracts/permission-groups.ts +++ b/apps/sim/lib/api/contracts/permission-groups.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { organizationIdSchema } from '@/lib/api/contracts/primitives' +import { shareAuthTypeSchema } from '@/lib/api/contracts/public-shares' import { defineRouteContract } from '@/lib/api/contracts/types' import { permissionGroupConfigSchema } from '@/lib/permission-groups/types' @@ -22,6 +23,7 @@ export const permissionGroupFullConfigSchema = z.object({ disableInvitations: z.boolean(), disablePublicApi: z.boolean(), disablePublicFileSharing: z.boolean(), + allowedFileShareAuthTypes: z.array(shareAuthTypeSchema).nullable(), hideDeployApi: z.boolean(), hideDeployMcp: z.boolean(), hideDeployA2a: z.boolean(), diff --git a/apps/sim/lib/api/contracts/public-shares.ts b/apps/sim/lib/api/contracts/public-shares.ts index 30f8015ddd0..aa623324558 100644 --- a/apps/sim/lib/api/contracts/public-shares.ts +++ b/apps/sim/lib/api/contracts/public-shares.ts @@ -4,9 +4,19 @@ import { defineRouteContract } from '@/lib/api/contracts/types' export const shareResourceTypeSchema = z.enum(['file', 'folder']) +/** How a public share is gated. */ +export const shareAuthTypeSchema = z.enum(['public', 'password', 'email', 'sso']) + +export type ShareAuthType = z.output + +/** An allowed email address or `@domain` pattern for email/SSO shares. */ +const allowedEmailSchema = z.string().min(1).max(320) + /** * Public-safe representation of a `public_share` row. Never carries the - * underlying storage key. + * underlying storage key or the (encrypted) password — `hasPassword` is the + * only password signal exposed to clients. `allowedEmails` is the allow-list for + * email/SSO shares (visible only to workspace members via the authed share route). */ export const shareRecordSchema = z.object({ id: z.string(), @@ -15,6 +25,9 @@ export const shareRecordSchema = z.object({ isActive: z.boolean(), resourceType: shareResourceTypeSchema, resourceId: z.string(), + authType: shareAuthTypeSchema, + hasPassword: z.boolean(), + allowedEmails: z.array(allowedEmailSchema), }) export type ShareRecord = z.output @@ -26,6 +39,21 @@ const fileShareParamsSchema = z.object({ export const upsertFileShareBodySchema = z.object({ isActive: z.boolean(), + authType: shareAuthTypeSchema.optional(), + password: z + .string() + .min(1, 'Password cannot be empty') + .max(1024, 'Password is too long') + .optional(), + allowedEmails: z.array(allowedEmailSchema).max(200, 'Too many allowed emails').optional(), + // Client-reserved token shown as the link before saving; persisted on first + // enable so a copied link resolves. Ignored once the share row already exists. + token: z + .string() + .regex(/^[A-Za-z0-9_-]+$/, 'Invalid token') + .min(16, 'Token is too short') + .max(64, 'Token is too long') + .optional(), }) export type UpsertFileShareBody = z.input @@ -97,3 +125,100 @@ export const getPublicFileContentContract = defineRouteContract({ mode: 'binary', }, }) + +const authenticatePublicFileBodySchema = z.object({ + password: z.string().min(1, 'Password is required').max(1024, 'Password is too long'), +}) + +export type AuthenticatePublicFileBody = z.input + +const authenticatePublicFileResponseSchema = z.object({ + authType: shareAuthTypeSchema, +}) + +export type AuthenticatePublicFileResponse = z.output + +/** + * Exchanges a share password for a `file_auth_{shareId}` cookie. IP rate-limited; + * returns 401 (`Invalid password`) on mismatch and 429 when throttled. + */ +export const authenticatePublicFileContract = defineRouteContract({ + method: 'POST', + path: '/api/files/public/[token]', + params: publicFileTokenParamsSchema, + body: authenticatePublicFileBodySchema, + response: { + mode: 'json', + schema: authenticatePublicFileResponseSchema, + }, +}) + +const requestPublicFileOtpBodySchema = z.object({ + email: z.string().email('Invalid email address'), +}) + +export type RequestPublicFileOtpBody = z.input + +const requestPublicFileOtpResponseSchema = z.object({ + message: z.string(), +}) + +/** Sends a 6-digit verification code to an allow-listed email for an email-gated share. */ +export const requestPublicFileOtpContract = defineRouteContract({ + method: 'POST', + path: '/api/files/public/[token]/otp', + params: publicFileTokenParamsSchema, + body: requestPublicFileOtpBodySchema, + response: { + mode: 'json', + schema: requestPublicFileOtpResponseSchema, + }, +}) + +const verifyPublicFileOtpBodySchema = requestPublicFileOtpBodySchema.extend({ + otp: z.string().length(6, 'Verification code must be 6 digits'), +}) + +export type VerifyPublicFileOtpBody = z.input + +const verifyPublicFileOtpResponseSchema = z.object({ + authType: shareAuthTypeSchema, +}) + +export type VerifyPublicFileOtpResponse = z.output + +/** Verifies the OTP and, on success, sets the `file_auth_{shareId}` cookie. */ +export const verifyPublicFileOtpContract = defineRouteContract({ + method: 'PUT', + path: '/api/files/public/[token]/otp', + params: publicFileTokenParamsSchema, + body: verifyPublicFileOtpBodySchema, + response: { + mode: 'json', + schema: verifyPublicFileOtpResponseSchema, + }, +}) + +const publicFileSSOBodySchema = z.object({ + email: z.string().email('Invalid email address'), +}) + +export type PublicFileSSOBody = z.input + +const publicFileSSOResponseSchema = z.object({ + eligible: z.boolean(), +}) + +export type PublicFileSSOResponse = z.output + +/** Reports whether an email is on the allow-list for an SSO-gated share. */ +export const publicFileSSOContract = defineRouteContract({ + method: 'POST', + path: '/api/files/public/[token]/sso', + params: publicFileTokenParamsSchema, + body: publicFileSSOBodySchema, + response: { + mode: 'json', + schema: publicFileSSOResponseSchema, + }, +}) diff --git a/apps/sim/lib/core/security/deployment-auth.ts b/apps/sim/lib/core/security/deployment-auth.ts new file mode 100644 index 00000000000..69c842def61 --- /dev/null +++ b/apps/sim/lib/core/security/deployment-auth.ts @@ -0,0 +1,202 @@ +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import type { NextRequest } from 'next/server' +import type { TokenBucketConfig } from '@/lib/core/rate-limiter' +import { RateLimiter } from '@/lib/core/rate-limiter' +import { + type DeploymentAuthKind, + deploymentAuthCookieName, + isEmailAllowed, + validateAuthToken, +} from '@/lib/core/security/deployment' +import { decryptSecret } from '@/lib/core/security/encryption' +import { getClientIp } from '@/lib/core/utils/request' + +const logger = createLogger('DeploymentAuth') + +const rateLimiter = new RateLimiter() + +/** + * Throttles unauthenticated password guesses per client IP against a single + * deployment, mirroring the OTP/SSO IP limits. + */ +const PASSWORD_IP_RATE_LIMIT: TokenBucketConfig = { + maxTokens: 10, + refillRate: 10, + refillIntervalMs: 15 * 60_000, +} + +/** + * A password/email-gated resource (a deployed chat or a public file share). Only + * the fields the auth check needs — the `password` is the encrypted secret. + */ +export interface DeploymentAuthResource { + id: string + authType: string | null + password?: string | null + allowedEmails?: unknown +} + +interface DeploymentAuthBody { + password?: string + email?: string + input?: unknown +} + +export interface DeploymentAuthResult { + authorized: boolean + error?: string + status?: number + retryAfterMs?: number +} + +/** + * Shared password/email/SSO gate for deployed resources. The `cookiePrefix` + * selects the auth cookie (`${cookiePrefix}_auth_${id}`) and the rate-limit + * namespace so chat deployments and public file shares share one code path. Both + * support all four modes: `'public'`, `'password'`, `'email'`, and `'sso'`. + */ +export async function validateDeploymentAuth( + requestId: string, + resource: DeploymentAuthResource, + request: NextRequest, + parsedBody: DeploymentAuthBody | null | undefined, + cookiePrefix: DeploymentAuthKind +): Promise { + const authType = resource.authType || 'public' + + if (authType === 'public') { + return { authorized: true } + } + + if (authType !== 'sso') { + const authCookie = request.cookies.get(deploymentAuthCookieName(cookiePrefix, resource.id)) + + if ( + authCookie && + validateAuthToken(authCookie.value, resource.id, authType, resource.password) + ) { + return { authorized: true } + } + } + + if (authType === 'password') { + if (request.method === 'GET') { + return { authorized: false, error: 'auth_required_password' } + } + + try { + if (!parsedBody) { + return { authorized: false, error: 'Password is required' } + } + + const { password, input } = parsedBody + + if (input && !password) { + return { authorized: false, error: 'auth_required_password' } + } + + if (!password) { + return { authorized: false, error: 'Password is required' } + } + + if (!resource.password) { + logger.error(`[${requestId}] No password set for password-protected ${resource.id}`) + return { authorized: false, error: 'Authentication configuration error' } + } + + const ip = getClientIp(request) + const ipRateLimit = await rateLimiter.checkRateLimitDirect( + `${cookiePrefix}-password:ip:${resource.id}:${ip}`, + PASSWORD_IP_RATE_LIMIT + ) + if (!ipRateLimit.allowed) { + logger.warn( + `[${requestId}] Password attempt IP rate limit exceeded for ${resource.id} from ${ip}` + ) + return { + authorized: false, + error: 'Too many attempts. Please try again later.', + status: 429, + retryAfterMs: ipRateLimit.retryAfterMs ?? PASSWORD_IP_RATE_LIMIT.refillIntervalMs, + } + } + + const { decrypted } = await decryptSecret(resource.password) + if (!safeCompare(password, decrypted)) { + return { authorized: false, error: 'Invalid password' } + } + + return { authorized: true } + } catch (error) { + logger.error(`[${requestId}] Error validating password:`, error) + return { authorized: false, error: 'Authentication error' } + } + } + + if (authType === 'email') { + if (request.method === 'GET') { + return { authorized: false, error: 'auth_required_email' } + } + + try { + if (!parsedBody) { + return { authorized: false, error: 'Email is required' } + } + + const { email, input } = parsedBody + + if (input && !email) { + return { authorized: false, error: 'auth_required_email' } + } + + if (!email) { + return { authorized: false, error: 'Email is required' } + } + + const allowedEmails = (resource.allowedEmails as string[]) || [] + + if (isEmailAllowed(email, allowedEmails)) { + return { authorized: false, error: 'otp_required' } + } + + return { authorized: false, error: 'Email not authorized' } + } catch (error) { + logger.error(`[${requestId}] Error validating email:`, error) + return { authorized: false, error: 'Authentication error' } + } + } + + if (authType === 'sso') { + try { + if (request.method !== 'GET' && !parsedBody) { + return { authorized: false, error: 'SSO authentication is required' } + } + + const { getSession } = await import('@/lib/auth') + const session = await getSession() + + if (!session || !session.user) { + return { authorized: false, error: 'auth_required_sso' } + } + + const userEmail = session.user.email + if (!userEmail) { + return { authorized: false, error: 'SSO session does not contain email' } + } + + const allowedEmails = (resource.allowedEmails as string[]) || [] + + if (isEmailAllowed(userEmail, allowedEmails)) { + return { authorized: true } + } + + return { authorized: false, error: 'Your email is not authorized to access this resource' } + } catch (error) { + logger.error(`[${requestId}] Error validating SSO:`, error) + return { authorized: false, error: 'SSO authentication error' } + } + } + + return { authorized: false, error: 'Unsupported authentication type' } +} diff --git a/apps/sim/lib/core/security/deployment.test.ts b/apps/sim/lib/core/security/deployment.test.ts new file mode 100644 index 00000000000..45032867c13 --- /dev/null +++ b/apps/sim/lib/core/security/deployment.test.ts @@ -0,0 +1,24 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { isEmailAllowed } from '@/lib/core/security/deployment' + +describe('isEmailAllowed', () => { + it('matches an exact email regardless of casing on either side', () => { + expect(isEmailAllowed('user@acme.com', ['user@acme.com'])).toBe(true) + expect(isEmailAllowed('User@Acme.com', ['user@acme.com'])).toBe(true) + expect(isEmailAllowed('user@acme.com', ['USER@ACME.COM'])).toBe(true) + expect(isEmailAllowed(' User@Acme.com ', ['user@acme.com'])).toBe(true) + }) + + it('matches a domain pattern regardless of casing (covers IdP/session emails)', () => { + expect(isEmailAllowed('User@Acme.com', ['@acme.com'])).toBe(true) + expect(isEmailAllowed('user@acme.com', ['@Acme.com'])).toBe(true) + }) + + it('rejects emails not on the allow-list', () => { + expect(isEmailAllowed('user@evil.com', ['user@acme.com', '@acme.com'])).toBe(false) + expect(isEmailAllowed('user@acme.com', [])).toBe(false) + }) +}) diff --git a/apps/sim/lib/core/security/deployment.ts b/apps/sim/lib/core/security/deployment.ts index 7bc03b2537b..9b102bd9527 100644 --- a/apps/sim/lib/core/security/deployment.ts +++ b/apps/sim/lib/core/security/deployment.ts @@ -38,6 +38,7 @@ function generateAuthToken( export function validateAuthToken( token: string, deploymentId: string, + authType: string, encryptedPassword?: string | null ): boolean { try { @@ -55,10 +56,15 @@ export function validateAuthToken( const parts = payload.split(':') if (parts.length < 4) return false - const [storedId, _type, timestamp, storedPwSlot] = parts + const [storedId, storedType, timestamp, storedPwSlot] = parts if (storedId !== deploymentId) return false + // Bind the cookie to the auth type so a token minted under one mode (e.g. a + // `public` share, which has an empty password slot) can't satisfy another + // mode (e.g. `email` OTP) after the share's auth type is changed. + if (storedType !== authType) return false + const expectedPwSlot = passwordSlot(encryptedPassword) if (storedPwSlot !== expectedPwSlot) return false @@ -72,19 +78,27 @@ export function validateAuthToken( } } +/** The kind of deployed resource an auth cookie/token belongs to. */ +export type DeploymentAuthKind = 'chat' | 'file' + +/** Canonical auth cookie name for a deployed resource (`{kind}_auth_{id}`). */ +export function deploymentAuthCookieName(cookiePrefix: DeploymentAuthKind, id: string): string { + return `${cookiePrefix}_auth_${id}` +} + /** * Sets an authentication cookie for a deployment */ export function setDeploymentAuthCookie( response: NextResponse, - cookiePrefix: 'chat', + cookiePrefix: DeploymentAuthKind, deploymentId: string, authType: string, encryptedPassword?: string | null ): void { const token = generateAuthToken(deploymentId, authType, encryptedPassword) response.cookies.set({ - name: `${cookiePrefix}_auth_${deploymentId}`, + name: deploymentAuthCookieName(cookiePrefix, deploymentId), value: token, httpOnly: true, secure: !isDev, @@ -95,17 +109,22 @@ export function setDeploymentAuthCookie( } /** - * Checks if an email matches the allowed emails list (exact match or domain match) + * Checks if an email matches the allowed emails list (exact match or domain + * match). Case-insensitive — email addresses are compared lowercased on both + * sides, so callers don't need to normalize before calling. */ export function isEmailAllowed(email: string, allowedEmails: string[]): boolean { - if (allowedEmails.includes(email)) { + const normalizedEmail = email.trim().toLowerCase() + const normalizedAllowed = allowedEmails.map((allowed) => allowed.trim().toLowerCase()) + + if (normalizedAllowed.includes(normalizedEmail)) { return true } - const atIndex = email.indexOf('@') + const atIndex = normalizedEmail.indexOf('@') if (atIndex > 0) { - const domain = email.substring(atIndex + 1) - if (domain && allowedEmails.some((allowed: string) => allowed === `@${domain}`)) { + const domain = normalizedEmail.substring(atIndex + 1) + if (domain && normalizedAllowed.some((allowed) => allowed === `@${domain}`)) { return true } } diff --git a/apps/sim/lib/core/security/otp.ts b/apps/sim/lib/core/security/otp.ts index a2055fd884c..326a48cec8a 100644 --- a/apps/sim/lib/core/security/otp.ts +++ b/apps/sim/lib/core/security/otp.ts @@ -7,10 +7,10 @@ import { getRedisClient } from '@/lib/core/config/redis' import type { TokenBucketConfig } from '@/lib/core/rate-limiter' import { getStorageMethod } from '@/lib/core/storage' -export type DeploymentKind = 'chat' +export type DeploymentKind = 'chat' | 'file' /** - * Shared OTP configuration for deployment (chat) email-auth gates. + * Shared OTP configuration for deployment email-auth gates (chat + public file shares). */ export const OTP_EXPIRY_SECONDS = 15 * 60 export const OTP_EXPIRY_MS = OTP_EXPIRY_SECONDS * 1000 @@ -38,6 +38,10 @@ const OTP_KEYS = { redisKey: (email: string, deploymentId: string) => `otp:${email}:${deploymentId}`, dbIdentifier: (email: string, deploymentId: string) => `chat-otp:${deploymentId}:${email}`, }, + file: { + redisKey: (email: string, deploymentId: string) => `otp:file:${email}:${deploymentId}`, + dbIdentifier: (email: string, deploymentId: string) => `file-otp:${deploymentId}:${email}`, + }, } as const satisfies Record< DeploymentKind, { diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index 809589d679f..3269ec69459 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -1,4 +1,8 @@ import { z } from 'zod' +import type { ShareAuthType } from '@/lib/api/contracts/public-shares' + +/** Auth modes a public file share can use; admins may restrict the allowed subset. */ +export const FILE_SHARE_AUTH_TYPES = ['public', 'password', 'email', 'sso'] as const export const PERMISSION_GROUP_CONSTRAINTS = { organizationName: 'permission_group_organization_name_unique', @@ -32,6 +36,7 @@ export const permissionGroupConfigSchema = z.object({ disableInvitations: z.boolean().optional(), disablePublicApi: z.boolean().optional(), disablePublicFileSharing: z.boolean().optional(), + allowedFileShareAuthTypes: z.array(z.enum(FILE_SHARE_AUTH_TYPES)).nullable().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), @@ -62,6 +67,8 @@ export interface PermissionGroupConfig { disableInvitations: boolean disablePublicApi: boolean disablePublicFileSharing: boolean + /** Allowed public-file-share auth modes; `null` means all are allowed. */ + allowedFileShareAuthTypes: ShareAuthType[] | null hideDeployApi: boolean hideDeployMcp: boolean hideDeployA2a: boolean @@ -88,6 +95,7 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { disableInvitations: false, disablePublicApi: false, disablePublicFileSharing: false, + allowedFileShareAuthTypes: null, hideDeployApi: false, hideDeployMcp: false, hideDeployA2a: false, @@ -125,6 +133,11 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf disablePublicApi: typeof c.disablePublicApi === 'boolean' ? c.disablePublicApi : false, disablePublicFileSharing: typeof c.disablePublicFileSharing === 'boolean' ? c.disablePublicFileSharing : false, + allowedFileShareAuthTypes: Array.isArray(c.allowedFileShareAuthTypes) + ? c.allowedFileShareAuthTypes.filter((t): t is ShareAuthType => + (FILE_SHARE_AUTH_TYPES as readonly string[]).includes(t as string) + ) + : null, hideDeployApi: typeof c.hideDeployApi === 'boolean' ? c.hideDeployApi : false, hideDeployMcp: typeof c.hideDeployMcp === 'boolean' ? c.hideDeployMcp : false, hideDeployA2a: typeof c.hideDeployA2a === 'boolean' ? c.hideDeployA2a : false, diff --git a/apps/sim/lib/public-shares/share-manager.ts b/apps/sim/lib/public-shares/share-manager.ts index ae502bd3333..67df205f79b 100644 --- a/apps/sim/lib/public-shares/share-manager.ts +++ b/apps/sim/lib/public-shares/share-manager.ts @@ -4,11 +4,24 @@ import { createLogger } from '@sim/logger' import { generateId, generateShortId } from '@sim/utils/id' import { and, eq, inArray, isNull } from 'drizzle-orm' import type { z } from 'zod' -import type { ShareRecord, shareResourceTypeSchema } from '@/lib/api/contracts/public-shares' +import type { + ShareAuthType, + ShareRecord, + shareResourceTypeSchema, +} from '@/lib/api/contracts/public-shares' +import { encryptSecret } from '@/lib/core/security/encryption' import { getBaseUrl } from '@/lib/core/utils/urls' const logger = createLogger('PublicShareManager') +/** Thrown when share auth config is invalid (e.g. enabling a password share with no password). Maps to a 400. */ +export class ShareValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'ShareValidationError' + } +} + type ShareResourceType = z.infer type PublicShareRow = typeof publicShare.$inferSelect @@ -26,6 +39,9 @@ function mapShareRecord(row: PublicShareRow): ShareRecord { isActive: row.isActive, resourceType: row.resourceType as ShareResourceType, resourceId: row.resourceId, + authType: row.authType as ShareAuthType, + hasPassword: Boolean(row.password), + allowedEmails: Array.isArray(row.allowedEmails) ? (row.allowedEmails as string[]) : [], } } @@ -71,19 +87,77 @@ interface UpsertFileShareInput { fileId: string userId: string isActive: boolean + /** Defaults to the existing share's authType (or `'public'` for a new share). */ + authType?: ShareAuthType + /** Plaintext password to set; encrypted at rest. Required to first enable a password share. */ + password?: string + /** Allowed emails/domains; required to enable an `email`/`sso` share without an existing list. */ + allowedEmails?: string[] + /** Client-reserved token to persist on first insert; ignored when the share already exists. */ + token?: string } /** * Enable or disable the public share for a file. First enable inserts a row with - * a fresh unguessable token; subsequent calls flip `isActive` and keep the token - * stable (so an existing link resolves again after re-enable). + * a fresh unguessable token; subsequent calls flip `isActive`/`authType` and keep + * the token stable (so an existing link resolves again after re-enable). + * + * Auth validation only applies when **enabling** (`isActive: true`): `password` + * requires a plaintext `password` unless one is already stored (encrypted via + * {@link encryptSecret}); `email`/`sso` require a non-empty `allowedEmails`. + * Disabling (going Private) always succeeds and preserves the stored config so a + * later re-enable restores it. Validation failures throw {@link ShareValidationError}. */ export async function upsertFileShare({ workspaceId, fileId, userId, isActive, + authType, + password, + allowedEmails, + token, }: UpsertFileShareInput): Promise { + const [existing] = await db + .select() + .from(publicShare) + .where(and(eq(publicShare.resourceType, 'file'), eq(publicShare.resourceId, fileId))) + .limit(1) + + const finalAuthType: ShareAuthType = + authType ?? (existing?.authType as ShareAuthType | undefined) ?? 'public' + const existingAllowedEmails = Array.isArray(existing?.allowedEmails) + ? (existing.allowedEmails as string[]) + : [] + + // Disabling preserves the stored config (and skips validation) so turning + // sharing off always succeeds; only enabling validates the chosen auth mode. + let finalPassword: string | null = existing?.password ?? null + let finalAllowedEmails: string[] = existingAllowedEmails + if (isActive) { + if (finalAuthType === 'password') { + if (password) { + finalPassword = (await encryptSecret(password)).encrypted + } else if (existing?.password) { + finalPassword = existing.password + } else { + throw new ShareValidationError('Password is required for password-protected shares') + } + finalAllowedEmails = [] + } else if (finalAuthType === 'email' || finalAuthType === 'sso') { + finalAllowedEmails = allowedEmails ?? existingAllowedEmails + if (finalAllowedEmails.length === 0) { + throw new ShareValidationError( + 'At least one allowed email is required for email/SSO shares' + ) + } + finalPassword = null + } else { + finalPassword = null + finalAllowedEmails = [] + } + } + const [row] = await db .insert(publicShare) .values({ @@ -92,16 +166,31 @@ export async function upsertFileShare({ resourceId: fileId, workspaceId, createdBy: userId, - token: generateShortId(), + token: token ?? generateShortId(), isActive, + authType: finalAuthType, + password: finalPassword, + allowedEmails: finalAllowedEmails, }) .onConflictDoUpdate({ target: [publicShare.resourceType, publicShare.resourceId], - set: { isActive, updatedAt: new Date() }, + set: { + isActive, + authType: finalAuthType, + password: finalPassword, + allowedEmails: finalAllowedEmails, + updatedAt: new Date(), + }, }) .returning() - logger.info('Upserted file share', { fileId, workspaceId, isActive, token: row.token }) + logger.info('Upserted file share', { + fileId, + workspaceId, + isActive, + authType: finalAuthType, + token: row.token, + }) return mapShareRecord(row) } diff --git a/packages/db/migrations/0245_public_share_auth.sql b/packages/db/migrations/0245_public_share_auth.sql new file mode 100644 index 00000000000..4c0a1274871 --- /dev/null +++ b/packages/db/migrations/0245_public_share_auth.sql @@ -0,0 +1,3 @@ +ALTER TABLE "public_share" ADD COLUMN "auth_type" text DEFAULT 'public' NOT NULL;--> statement-breakpoint +ALTER TABLE "public_share" ADD COLUMN "password" text;--> statement-breakpoint +ALTER TABLE "public_share" ADD COLUMN "allowed_emails" json DEFAULT '[]'; \ No newline at end of file diff --git a/packages/db/migrations/meta/0245_snapshot.json b/packages/db/migrations/meta/0245_snapshot.json new file mode 100644 index 00000000000..ccc71ec8249 --- /dev/null +++ b/packages/db/migrations/meta/0245_snapshot.json @@ -0,0 +1,16755 @@ +{ + "id": "b41eaa3c-968a-4809-982d-d9879fa2db48", + "prevId": "c5ce36a0-5b8d-4534-ad33-5631beab1130", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index c1e8428e8c1..03f0c71e5ea 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1709,6 +1709,13 @@ "when": 1781899910981, "tag": "0244_table_row_executions_enrichment_details", "breakpoints": true + }, + { + "idx": 245, + "version": "7", + "when": 1781904859472, + "tag": "0245_public_share_auth", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 016684b0d17..04509714af1 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1445,6 +1445,12 @@ export const publicShare = pgTable( createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), token: text('token').notNull(), isActive: boolean('is_active').notNull().default(true), + // 'public' (anyone with the link) | 'password' | 'email' (OTP) | 'sso'. + authType: text('auth_type').notNull().default('public'), + // AES-256-GCM encrypted share password; null unless authType is 'password'. + password: text('password'), + // Allowed emails/domains (e.g. '@acme.com') when authType is 'email' or 'sso'. + allowedEmails: json('allowed_emails').default('[]'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index b294838bac0..09744c629ba 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 857, - zodRoutes: 857, + totalRoutes: 859, + zodRoutes: 859, nonZodRoutes: 0, } as const From ecbe1919d8525e68f28e5c54faf03c52439e0bcd Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 19 Jun 2026 18:32:42 -0700 Subject: [PATCH 14/16] feat(files): inline rich markdown editor (#5133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(files): inline rich markdown editor Replace the raw/preview split for markdown files with a Linear-style inline WYSIWYG editor (TipTap/ProseMirror): bubble + slash menus, code-block language picker with Prism highlighting and line-wrap, resizable images (HTML ), GFM tables, and frontmatter held byte-exact out of band. A round-trip preflight gate (decided once per open) falls back to the raw Monaco editor for any file that can't be edited losslessly, so the rich editor never silently corrupts a file. * fix(files): chain autosave unmount flush after in-flight save The unmount flush no longer fires a concurrent PUT alongside an in-flight save; it awaits the in-flight save and then writes the latest content sequentially, so an out-of-order completion can't clobber newer edits with a stale snapshot (addresses Cursor Bugbot). * fix(files): read pasted images from clipboard items, not just files Some browsers expose a pasted or copied image only via DataTransfer.items (with an empty files list), so screenshot paste was silently ignored. extractImageFiles now falls back to items; moved to a testable module with unit tests (addresses Cursor Bugbot). * fix(files): destroy round-trip probe editor on serialization error Wrap the probe serialize() in try/finally so the throwaway Editor is always destroyed even if setContent/getMarkdown throws (addresses Greptile). Adds a test proving PipeSafeTable escapes only interior cell pipes, not structural delimiters. * fix(resource): hold breadcrumb nav latch across the route swap scheduleClose fired on the pointer/focus exit that immediately follows a click-to-navigate and was clearing the reopen latch before the route swapped, letting the popover flash back open. The latch is now released by a short timer instead (addresses Cursor Bugbot). * chore(files): drop platform references and non-essential inline comments * fix(files): scope inline markdown editor to the files view The mothership preview was routing streaming markdown through the inline editor path: it showed Monaco during streaming (previewMode fell back to 'editor') and lost the streamed content on the TextEditor→MarkdownFileEditor swap (the TextEditor unmounted before it could reconcile + autosave). The inline rich editor is now opt-in via a FileViewer prop that only the files view sets, so the mothership keeps its raw/preview streaming editor and persists as before. * fix(mothership): use the inline markdown editor in the chat resource view Idle markdown in the chat resource view now renders the single-surface inline editor (no raw/split/preview pencil toggle), matching the files view. While the agent streams, FileViewer forces the rendered preview instead of Monaco, and the streamed file persists via the agent's server write + the existing content-query invalidation on tool completion — so the idle editor refetches the persisted content. * refactor(files): collapse the duplicate raw-editor fallback branch in the markdown gate * fix(mothership): swap to the inline editor once a file preview finishes streaming The preview session keeps status='complete' and previewText after streaming ends, so streamingContent stayed defined and the file stuck on the read-only rendered preview. Treat content as streaming only while status==='streaming'; once complete the EmbeddedFile sees no streamingContent and mounts the editable inline editor (which refetches the persisted content). The synthetic streaming-file stays a pure preview. * Revert "fix(mothership): swap to the inline editor once a file preview finishes streaming" This reverts commit 25b12e4caa389109c2d5ed7f7d122e35d441f980. * Revert "fix(mothership): use the inline markdown editor in the chat resource view" This reverts commit 9430aa7fdc5050d002f2312d31dcf4a255e2de18. * feat(files): rich markdown editor across files + chat, read-only for unsafe, robust load/save - chat resource view streams into the rich editor (streamdown while streaming → editable on completion); agent persists server-side, editor never saves mid-stream - round-trip-unsafe / >128KB markdown renders read-only in the rich editor (no Monaco, no corruption) - markdown always uses the rich editor (dropped the inline-markdown opt-in flag) - editor loads content as TipTap's initial content keyed by file id — strict-mode/SSR-safe, no content-sync effect - fix autosave "Saving…" status suppression under React strict mode - lock the streamed-file persistence handoff with a state-machine lifecycle test * chore(files): remove dead code (unused FileViewer logger + EmbeddedWorkflowActions router) * fix(files): derive markdown round-trip verdict from live content, not a locked stale snapshot The gate locked isRoundTripSafe on the first post-stream snapshot, which is often the empty create_file buffer before the agent's server write lands — wrongly leaving an unsafe document editable. Derive the verdict from the current content (memoized on the bytes) so canEdit tracks the real payload. * test(files): guard the rich editor dirty signal — open is never dirty, edits emit * fix(files): lock the markdown round-trip verdict on opened content, never strand dirty edits The round-trip-safety verdict now gates editability only at open time — computed once, on the exact content the editor mounts with, and locked for its lifetime. A dirty document is round-trip-safe by construction (the editor only emits safe markdown), so the verdict must never flip off mid-edit: doing so disabled autosave, ⌘S, the toolbar Save and the unmount flush, stranding unsaved edits. Locking on the opened (reconciled) content also fixes the stale post-stream empty-buffer snapshot, and lets the redundant MarkdownFileEditor gate (plus its duplicate content fetch) be deleted. * improvement(file-viewer): reuse shared copy hook, lazy frontmatter split - code-block: replace hand-rolled copy-with-timeout with shared useCopyToClipboard - rich-markdown-editor: compute frontmatter split once via lazy ref, drop redundant frontmatterRef - round-trip-safety: correct stale comments (read-only, not raw editor fallback) * feat(file-viewer): linked images, typed-link input rule, drag-to-reorder, churn fixes - image: round-trip linked images/badges via an href attr + custom markdown tokenizer; make the image a drag handle so it can be grabbed and reordered - link-input-rule: convert typed [text](url) to a link on the closing paren (normalized href) - markdown-paste: render pasted markdown as rich content, guarded against code blocks - round-trip-safety: behavioral link-count check replaces the static linked-image rejection - extensions: trim the table serializer's blank lines to stop interior-table whitespace churn * improvement(file-viewer): Backspace at start of a heading reverts it to a paragraph Notion-style: ProseMirror's default joins or no-ops at a heading boundary, stranding the heading style. A second Backspace then merges as usual. * fix(file-viewer): don't upload pasted/dropped images into a read-only editor handlePaste/handleDrop ran the workspace image upload without checking editability, so a read-only doc (canEdit=false or a round-trip-unsafe file) could still trigger an upload. Guard both on view.editable. * fix(file-viewer): sanitize linked-image href; drop global leading-newline strip - image: run the linked-image (badge) anchor target through normalizeLinkHref so a javascript:/data: href in a file can't execute on click; the markdown still preserves the raw target (file content unchanged) - markdown-fidelity: the table serializer now trims its own surrounding blank lines, so the global leading-newline strip in postProcessSerializedMarkdown is redundant — removing it stops clobbering content that legitimately begins with whitespace * feat(file-viewer): stream agent output directly into the rich editor; add more code languages - rich-markdown-editor: the TipTap editor is now the only markdown surface. Agent output streams into it read-only (synced per chunk, autoscrolled), then the same instance hands off to an editable editor on settle — no separate streamdown preview, so no stream→edit flash. The round-trip verdict + frontmatter lock when the content settles. - code-block/code-highlight/detect-language: register Go, Rust, Java, C, C++, C#, Ruby, PHP grammars and add detectors, so those blocks highlight and the picker offers them. - css: style h5/h6 in the prose stylesheet. * fix(sidebar): hydrate collapse state before paint to stop refresh flash The collapsed sidebar swaps entire subtrees (collapsed flyout vs expanded lists), but isCollapsed only resolved after the first paint via auto rehydration, so a collapsed reload rendered the expanded tree into the 51px rail and then reflowed — the misplaced/flashing content on refresh. Adopt zustand's documented SSR pattern: skipHydration on the persist config (first render keeps the default false, matching SSR HTML) and flush persist.rehydrate() from a useLayoutEffect so the correct structure commits in the same pre-paint frame. Removes the old race where onRehydrateStorage lifted the data-sidebar-collapsed mask before React committed the rail. * refactor(file-viewer): audit fixes — stale docs, DRY settle-lock, language detection - rich-markdown-editor: rewrite the now-stale single-surface docstring (no PreviewPanel); extract a shared lockSettled() helper used by both the mount and stream-settle paths; guard the settle re-seed so it only setContent's when the body actually changed (no redundant doc rebuild) - detect-language: stop misreading generics (List) as HTML markup; detect Go type/struct - code-block: export LANGUAGE_OPTIONS + add a test asserting every picker language has a registered Prism grammar (prevents picker/highlighter drift) * refactor(file-viewer): remove dead markdown-preview renderer now superseded by the rich editor Markdown files route exclusively to RichMarkdownEditor on both the read-only and editable paths, so PreviewPanel's markdown branch and its Streamdown-based renderer were unreachable. Delete MarkdownPreview and its renderers, callout/ frontmatter/checkbox machinery, and the now-unused remark/rehype/prism/streamdown imports; drop the dead toggleMarkdownCheckbox/onCheckboxToggle plumbing in text-editor. Keep the html/csv/svg/mermaid branches intact. * refactor(file-viewer): drop dead streamingMode/append path, align naming, cover autosave The streaming engine only ever runs in 'replace' mode (the only runtime callers pass it); the 'append' branch of resolveStreamingEditorContent was unreachable. Remove streamingMode + the StreamingMode type and thread it out of the 6 components that forwarded it — nextContent is now simply the streamed snapshot, behavior-identical on the live path. Rename for codebase semantics: the boolean prop streaming -> isStreaming, EditorKeymap -> RichMarkdownKeymap, the highlight PluginKey KEY -> HIGHLIGHT_PLUGIN_KEY. Add a defensive isEditable guard to the markdown paste handler (parity with the image handler; read-only must never mutate). Add a dependency-free useAutosave test suite (debounce, min-display window, no-data-loss when an edit lands mid-save, error/no-retry, Cmd+S flush, streaming-disabled lock, unmount flush). * fix(file-viewer): re-lock round-trip verdict + frontmatter on each stream settle LoadedRichMarkdownEditor stays mounted across multiple agent edits to the same file within a chat (previewContextKey is the chat id), but the settle effect only locked settledRef when it was null — so a second stream into the same instance kept editability and frontmatter tied to the first settled snapshot. A repeat edit that is round-trip-unsafe would stay editable, and saves would re-attach the stale frontmatter. Track wasStreaming and re-derive the verdict + frontmatter on every stream->settle transition (user edits never re-derive, preserving the don't-strand-edits rule). Verified red/green in the e2e streaming harness. * test(file-viewer): lock link href sanitization for dangerous schemes from file content Greptile flagged a possible javascript: link XSS. Verified TipTap 3.26.1 already neutralizes javascript:/data:/vbscript: (and mixed-case/whitespace variants) from file-loaded markdown to an empty href. Add a committed regression test that asserts this against the real headless editor, so a future TipTap bump can't silently reintroduce the issue. * perf(file-viewer): cap the round-trip probe at 24KB and coalesce streaming syncs @tiptap/markdown's parse is superlinear (~O(n2)) in document size — measured ~170ms at 11KB, ~875ms at 23KB, multiple seconds past ~35KB — and it runs synchronously at mount inside the round-trip-safety probe (twice) and the editor's own setContent. The 128KB cap allowed multi-second main-thread freezes; lower it to 24KB so the worst-case mount stays near a second while still covering the vast majority of real markdown files (larger files open read-only). Separately, coalesce streaming chunk-syncs to one re-parse per animation frame so a fast-streaming agent doesn't re-parse the whole accumulating doc per token. Typing latency was measured to be already excellent (sub-ms median, no change needed); the only hot cost was the mount parse. * perf(file-viewer): chunked markdown parsing to remove the O(n2) mount cost @tiptap/markdown's whole-document setContent(md,'markdown') is superlinear in size, freezing the main thread at mount for large files (~2.5s at 34KB, ~11s at 65KB) and forcing a restrictive read-only cap. Parse block-by-block instead: a conservative blank-line/fence-aware splitter (merges list/quote runs and indented continuations so ambiguous structures stay atomic; reference-link/footnote/raw-HTML docs fall back to a whole parse), each block parsed with the editor's own lexer via one reused headless parser, assembled into a doc. This is linear and byte-identical to the one-shot parse — measured ~15ms vs multiple seconds at 124KB+ — so the editor mount, streaming sync, and round-trip probe are all linear, and the editable-size cap goes 24KB -> 256KB (covers the p99 of real files). Fidelity + idempotency are pinned by unit tests, a 400-document property/fuzz test, and adversarial edge cases (nested/loose lists, blockquotes, setext, indented code, lazy continuation, HTML, reference links). * fix(sidebar): render collapse state from a cookie so SSR matches The server couldn't read localStorage, so a collapsed user's first paint rendered the *expanded* tree at 51px — prefetched chat/workflow lists, pinned-chat pin icons, and loading skeletons all crammed into the rail and then reflowed once the store hydrated. Mirror the collapse state into a sidebar_collapsed cookie (the shadcn/ui sidebar pattern), read it in the workspace server layout, and seed the sidebar's first render with it: structure is now correct on the server, so the first paint is the real rail with no skeleton/pin/shift. The store remains the post-hydration source of truth; the blocking script honors the cookie for width when localStorage is absent so width and structure agree. * refactor(sidebar): make the cookie the single source of truth for collapse Consolidates the collapse machinery onto one source of truth instead of layering the cookie on top of the legacy localStorage + CSS-mask system: - Collapse persists only in the sidebar_collapsed cookie; the store seeds isCollapsed from it and drops it from localStorage (partialize + merge), removing the dual-write and the cross-tab desync it caused. - Retire the redundant html[data-sidebar-collapsed] attribute + CSS mask now that the server emits the correct data-collapsed structure; also delete the dead sidebar-collapse-show/-remove/-btn rules. - Blocking script reads the cookie for collapse (width stays in localStorage) and seeds the cookie once from the legacy flag so existing collapsed users keep their preference. - Keep skipHydration + a pre-paint rehydrate for width only — the documented zustand SSR pattern, so _hasHydrated is deterministically false during SSR. Width stays in localStorage; each field now has exactly one home. * refactor(file-viewer): simplify + cleanup chunked-parse (linear merge, parse-once seed) From the /simplify + /cleanup passes: - splitMarkdownBlocks: build continuation runs and join each once instead of concatenating onto the growing previous block per group, which was O(n2) for a pathological single long loose list (now linear: 208KB loose list splits in ~3ms). - rich-markdown-editor: seed the editor's initial content via a lazy useState initializer instead of useRef(parseMarkdownToDoc(...)), whose argument re-parsed the whole document on every render (i.e. every keystroke). Parses exactly once at mount. - Document that the indent-merge rule is load-bearing for nested fenced code, and tighten the verbose inline comment blocks. * refactor(sidebar): drop orphaned sidebar-collapse-btn class Its CSS rule was removed with the data-sidebar-collapsed mask; the button's collapse behavior is fully driven by the React isCollapsed ternary, leaving the class name pointing at nothing. * test(file-viewer): consolidate split test files into one per module Match the dir's one-test-per-module convention: fold the markdown-parse property/fuzz suite into markdown-parse.test.ts and the editability corpus into round-trip-safety.test.ts (both already tested the same module from a separate-concern file). No coverage change — same assertions, fewer files (12 -> 10). * fix(file-viewer): make all editor controls respect read-only permissions Every interactive control that calls updateAttributes/dispatches a command mutates the doc even when read-only (ProseMirror commands run regardless of editable), so gate them on editor.isEditable: - bubble menu: the Cmd/Ctrl+K shortcut and shouldShow now bail when not editable, so a read-only doc can't open the link bar and setLink into it (Cursor finding). - code block: the language picker renders as a static label when read-only (its onSelect mutates); copy + view-only wrap stay. - image: no drag-to-reorder (draggable=false, no drag handle) and no resize handle when read-only; the image still renders and follows its link. - links: a plain click now follows the link in read-only (reader) mode, while edit mode still requires a modifier so a plain click can place the cursor (Cursor finding). Verified with new read-only permission e2e tests. * fix(sidebar): honor collapsed cookie even when localStorage is corrupt The blocking script read the collapse cookie inside the same try as JSON.parse(localStorage); invalid persisted JSON fell through to the 248px fallback and ignored a collapsed cookie, painting an expanded-width rail on first load. Read collapse from the cookie first and parse the persisted width in its own try so the two are independent. * docs(sidebar): convert inline comments to TSDoc * fix(file-viewer): resolve in-app workspace image URLs in the rich editor The removed MarkdownPreview rewrote /workspace/{id}/files/{fileId} image src to the serving endpoint /api/files/view/{fileId}; without it, in-app image URLs 404 in the rich editor (Cursor finding). Re-add the rewrite as a display-only transform on the rendered — the node's stored src attribute keeps the original path so markdown round-trips unchanged. Absolute/non-workspace URLs pass through. Unit tested. * fix(files): restore same-page anchor links in the rich markdown editor Headings rendered by the TipTap editor had no slug ids (the old MarkdownPreview got them from rehype-slug), so in-document table-of-contents links like [section](#section) had no targets. Resolve the slug to its heading on click (GitHub-style, duplicate-disambiguated) and scroll to it, with zero per-keystroke cost. * feat(files): render mermaid diagrams in the rich markdown editor A code block renders as a Mermaid diagram when it is fenced ```mermaid or auto-detected (an untagged fence whose first line opens with a diagram keyword, the Linear/GitHub heuristic). Detection is display-only — the node stays an ordinary code block and the markdown round-trips unchanged. - Source while the caret is inside the block, diagram on blur; a Show source / Show diagram control plus copy, matching the code block's hover chrome. - Clicking the diagram selects the node (same ring as an image), not flips source. - Theme-aware (light/dark) via next-themes; the diagram frame shares the code block's chrome (one CSS source of truth). - Extracted MermaidDiagram into a shared module so the editor reuses it without pulling preview-panel's heavy deps; rendered SVGs are memoized so toggling the source view and back is instant. Covered by mermaid-diagram unit tests and the editor e2e harness. * fix(files): harden the markdown editor (CRLF chunking, href allowlist, image escaping) Final-audit follow-ups: - splitMarkdownBlocks normalizes CRLF/CR first — a closing fence ending in \r no longer fails to match, which had collapsed Windows-authored files with fenced code into one block and defeated the linear chunker (perf regression). - normalizeLinkHref rejects file://, blob:, and other non-network schemes (script/data schemes already rejected); network scheme:// (http/ftp/…) and bare host:port still pass. - Image markdown serialization escapes alt/title delimiters and angle-brackets a src with spaces/parens, so they round-trip losslessly; linked-image anchors open in a new tab (target=_blank). - Markdown paste routes through the chunker so a large pasted blob can't freeze the main thread. * test(files): cover the code-highlight incremental re-tokenization gate Export and unit-test changeTouchesCodeBlock: prose-only edits map decorations (false), edits inside a code block or a setNodeMarkup language change re-tokenize (true) — the perf-correctness path that keeps highlighting off the keystroke path. * fix(files): keep relative links relative, navigate in-app links within the SPA - normalizeLinkHref no longer prefixes `./`/`../` relative paths into `https://./…` (they round-trip and resolve correctly). - Following a same-origin in-app link (e.g. /workspace/…) routes through the Next router (same tab) instead of always opening a new tab; modifier-click and external URLs still open a new tab. * fix(files): linked images don't open a tab on a plain click in the editor The linked-image anchor's native navigation was firing on a plain click in edit mode (where handleClick intentionally returns false for caret placement). Prevent the anchor's default so the editor's handleClick — gated on editable/modifier, matching text links via openOnClick:false — is the sole navigator. * fix(sidebar): match the collapse cookie value strictly (not a substring) A substring search for 'sidebar_collapsed=1' also matched 'sidebar_collapsed=10', desyncing the pre-paint sidebar rail and client store from the strict server read. Parse the cookie value and compare it to '1' exactly, in both the pre-paint inline script and readCollapsedCookie. Added a store test. * fix(sidebar): reconcile migrated-legacy collapse before paint A user whose collapse lived only in localStorage has no sidebar_collapsed cookie at SSR (initialCollapsed=false), but the pre-paint script migrates them to a cookie. The store's persist.rehydrate() is async (flips _hasHydrated after paint), so the first paint showed expanded labels in the collapsed 51px rail. Reconcile to the cookie synchronously in a useLayoutEffect (first render still matches the server, so no hydration mismatch) — no narrow-rail flash. --- apps/sim/app/_styles/globals.css | 29 +- apps/sim/app/layout.tsx | 48 +- .../resource-header/resource-header.tsx | 52 +- .../workspace-chrome/workspace-chrome.tsx | 6 +- .../components/file-viewer/file-viewer.tsx | 52 +- .../files/components/file-viewer/index.ts | 8 +- .../file-viewer/mermaid-diagram.test.ts | 63 ++ .../file-viewer/mermaid-diagram.tsx | Bin 0 -> 7062 bytes .../components/file-viewer/preview-panel.tsx | 924 +----------------- .../rich-markdown-editor/code-block.tsx | 262 +++++ .../code-highlight.test.ts | 81 ++ .../rich-markdown-editor/code-highlight.ts | 133 +++ .../code-languages.test.ts | 21 + .../detect-language.test.ts | 38 + .../rich-markdown-editor/detect-language.ts | 63 ++ .../rich-markdown-editor/dirty-signal.test.ts | 55 ++ .../rich-markdown-editor/extensions.ts | 113 +++ .../heading-anchors.test.ts | 57 ++ .../rich-markdown-editor/heading-anchors.ts | 36 + .../rich-markdown-editor/image-paste.test.ts | 56 ++ .../rich-markdown-editor/image-paste.ts | 14 + .../rich-markdown-editor/image.test.ts | 27 + .../rich-markdown-editor/image.tsx | 283 ++++++ .../rich-markdown-editor/keymap.ts | 70 ++ .../link-input-rule.test.ts | 70 ++ .../rich-markdown-editor/link-input-rule.ts | 45 + .../rich-markdown-editor/markdown-fidelity.ts | 75 ++ .../markdown-parse.test.ts | 212 ++++ .../rich-markdown-editor/markdown-parse.ts | 143 +++ .../markdown-paste.test.ts | 73 ++ .../rich-markdown-editor/markdown-paste.ts | 55 ++ .../menus/bubble-menu.tsx | 293 ++++++ .../rich-markdown-editor.css | 293 ++++++ .../rich-markdown-editor.tsx | 384 ++++++++ .../round-trip-safety.test.ts | 277 ++++++ .../rich-markdown-editor/round-trip-safety.ts | 101 ++ .../rich-markdown-editor/round-trip.test.ts | 319 ++++++ .../slash-command/commands.test.ts | 51 + .../slash-command/commands.ts | 147 +++ .../slash-command/slash-command-list.tsx | 129 +++ .../slash-command/slash-command.ts | 111 +++ .../file-viewer/text-editor-state.test.ts | 130 ++- .../file-viewer/text-editor-state.ts | 36 +- .../components/file-viewer/text-editor.tsx | 170 +--- .../file-viewer/use-editable-file-content.ts | 187 ++++ .../workspace/[workspaceId]/files/files.tsx | 17 +- .../resource-content/resource-content.tsx | 10 +- .../mothership-view/mothership-view.tsx | 6 +- .../app/workspace/[workspaceId]/layout.tsx | 6 +- .../w/components/sidebar/sidebar.tsx | 46 +- apps/sim/hooks/use-autosave.test.tsx | 267 +++++ apps/sim/hooks/use-autosave.ts | 72 +- apps/sim/package.json | 12 + apps/sim/stores/sidebar/store.test.ts | 34 + apps/sim/stores/sidebar/store.ts | 45 +- bun.lock | 122 ++- 56 files changed, 5131 insertions(+), 1298 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-languages.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/dirty-signal.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-input-rule.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-input-rule.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-parse.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-parse.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/slash-command-list.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/slash-command.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts create mode 100644 apps/sim/hooks/use-autosave.test.tsx create mode 100644 apps/sim/stores/sidebar/store.test.ts diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 0baeb6d70a1..d8765bb9fdd 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -66,38 +66,11 @@ opacity: 0; } -html[data-sidebar-collapsed] .sidebar-container span, -html[data-sidebar-collapsed] .sidebar-container .text-small { - opacity: 0; -} - .sidebar-container .sidebar-collapse-hide { transition: opacity 60ms ease; } -.sidebar-container .sidebar-collapse-show { - opacity: 0; - pointer-events: none; - transition: opacity 120ms ease-out; -} - -.sidebar-container[data-collapsed] .sidebar-collapse-hide, -html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-hide { - opacity: 0; -} - -.sidebar-container[data-collapsed] .sidebar-collapse-show, -html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-show { - opacity: 1; - pointer-events: auto; -} - -html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-remove { - display: none; -} - -html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-btn { - width: 0; +.sidebar-container[data-collapsed] .sidebar-collapse-hide { opacity: 0; } diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 82e6f107b77..4ab0bddef79 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -78,26 +78,36 @@ export default function RootLayout({ children }: { children: React.ReactNode }) // window yields a width >= MIN instead of a sub-minimum sliver. var defaultSidebarWidth = 248; try { - var stored = localStorage.getItem('sidebar-state'); - if (stored) { - var parsed = JSON.parse(stored); - var state = parsed && parsed.state; - var isCollapsed = state && state.isCollapsed; - - if (isCollapsed) { - document.documentElement.style.setProperty('--sidebar-width', '51px'); - document.documentElement.setAttribute('data-sidebar-collapsed', ''); - } else { - var width = state && state.sidebarWidth; - var maxSidebarWidth = Math.max(248, window.innerWidth * 0.3); - var finalWidth = - typeof width === 'number' && isFinite(width) - ? Math.min(Math.max(width, 248), maxSidebarWidth) - : defaultSidebarWidth; - document.documentElement.style.setProperty('--sidebar-width', finalWidth + 'px'); - } + // Collapse comes from the cookie (independent of localStorage + // parsing); the persisted width is read defensively below. Match the + // value strictly so 'sidebar_collapsed=10' isn't read as collapsed. + var cookieMatch = document.cookie.match(/(?:^|;\s*)sidebar_collapsed=([^;]*)/); + var hasCookie = cookieMatch !== null; + var collapsed = cookieMatch !== null && cookieMatch[1] === '1'; + + var state = null; + try { + var stored = localStorage.getItem('sidebar-state'); + state = stored ? JSON.parse(stored).state : null; + } catch (e) {} + + // One-time migration: seed the cookie from the legacy localStorage + // flag for users who collapsed before the cookie existed. + if (!hasCookie && state && typeof state.isCollapsed === 'boolean') { + collapsed = state.isCollapsed; + document.cookie = 'sidebar_collapsed=' + (collapsed ? '1' : '0') + '; path=/; max-age=31536000; samesite=lax'; + } + + if (collapsed) { + document.documentElement.style.setProperty('--sidebar-width', '51px'); } else { - document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth + 'px'); + var width = state && state.sidebarWidth; + var maxSidebarWidth = Math.max(248, window.innerWidth * 0.3); + var finalWidth = + typeof width === 'number' && isFinite(width) + ? Math.min(Math.max(width, 248), maxSidebarWidth) + : defaultSidebarWidth; + document.documentElement.style.setProperty('--sidebar-width', finalWidth + 'px'); } } catch (e) { document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth + 'px'); diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx index b544c525cae..f03a8cdcdf2 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx @@ -371,6 +371,14 @@ interface BreadcrumbLocationPopoverProps { veilBoundaryRef: React.RefObject } +/** + * Grace period before a hover-out dismisses the path popover. Covers the gap + * the pointer crosses between the trigger and the popover content (and brief + * jitter at their edges); re-entering either within this window cancels the + * close. Standard hover-intent close delay — not tied to any navigation timing. + */ +const POPOVER_CLOSE_DELAY_MS = 120 + function BreadcrumbLocationPopover({ icon: Icon, breadcrumbs, @@ -381,22 +389,44 @@ function BreadcrumbLocationPopover({ const closeTimeoutRef = useRef | null>(null) const rootBreadcrumb = breadcrumbs[0] - const openPopover = () => { + const cancelScheduledClose = () => { if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current) closeTimeoutRef.current = null } + } + + /** + * Hover-intent open. Driven only by pointer-/keyboard-enter — never by + * pointer movement. This is what makes the popover dismiss cleanly on a + * click-to-navigate: a stationary click fires no enter event, so once + * {@link navigateAndClose} sets `open` false nothing re-opens it before the + * route swaps. (A move-driven open would re-fire under the resting cursor and + * flash the popover/veil back in mid-navigation.) + */ + const openPopover = () => { + cancelScheduledClose() setOpen(true) } const scheduleClose = () => { - if (closeTimeoutRef.current) { - clearTimeout(closeTimeoutRef.current) - } + cancelScheduledClose() closeTimeoutRef.current = setTimeout(() => { setOpen(false) closeTimeoutRef.current = null - }, 120) + }, POPOVER_CLOSE_DELAY_MS) + } + + /** + * Closes the popover up front, then runs the crumb's handler. Closing first + * lets the veil fade and the popover play its exit animation instead of + * snapping away when navigation unmounts the header. + */ + const navigateAndClose = (onClick?: () => void) => { + if (!onClick) return + cancelScheduledClose() + setOpen(false) + onClick() } useEffect(() => { @@ -413,15 +443,11 @@ function BreadcrumbLocationPopover({
import('./pdf-viewer').then((m) => m.PdfViewerCore), { ssr: false, }) -const logger = createLogger('FileViewer') +const RichMarkdownEditor = dynamic( + () => import('./rich-markdown-editor/rich-markdown-editor').then((m) => m.RichMarkdownEditor), + { ssr: false, loading: () => } +) /** * CSVs at or below this size load fully into the editor (editable, with an inline preview). @@ -50,6 +48,15 @@ export function isPreviewable(file: { type: string; name: string }): boolean { return resolvePreviewType(file.type, file.name) !== null } +/** + * Markdown files render in the inline rich editor ({@link RichMarkdownEditor}) rather than + * the raw Monaco editor. Toolbars use this to hide the raw/split/preview mode controls, + * which don't apply to the single-surface editor. + */ +export function isMarkdownFile(file: { type: string; name: string }): boolean { + return resolvePreviewType(file.type, file.name) === 'markdown' +} + /** * A CSV larger than {@link CSV_INLINE_EDIT_MAX_BYTES} is shown as a streamed, read-only preview — * the editor would OOM loading the whole file. The viewer renders {@link CsvTablePreview} for it, @@ -84,7 +91,6 @@ interface FileViewerProps { onSaveStatusChange?: (status: 'idle' | 'saving' | 'saved' | 'error') => void saveRef?: React.MutableRefObject<(() => Promise) | null> streamingContent?: string - streamingMode?: StreamingMode disableStreamingAutoScroll?: boolean previewContextKey?: string } @@ -100,7 +106,6 @@ export function FileViewer({ onSaveStatusChange, saveRef, streamingContent, - streamingMode, disableStreamingAutoScroll = false, previewContextKey, }: FileViewerProps) { @@ -114,6 +119,14 @@ export function FileViewer({ if (isCsvStreamOnly(file)) { return } + // Markdown renders through the inline rich editor (non-editable) so the public share + // surface matches the in-app reading experience; canEdit={false} disables autosave, + // the bubble menu, and every other editing affordance. + if (isMarkdownFile(file)) { + return ( + + ) + } return } // A large CSV can't be loaded whole into the editor (the browser OOMs on the full text). @@ -122,6 +135,24 @@ export function FileViewer({ return } + if (isMarkdownFile(file)) { + return ( + + ) + } + return ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts index d41898d004a..5230f8c9560 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts @@ -1,4 +1,10 @@ export { resolveFileCategory } from './file-category' export type { PreviewMode } from './file-viewer' -export { FileViewer, isCsvStreamOnly, isPreviewable, isTextEditable } from './file-viewer' +export { + FileViewer, + isCsvStreamOnly, + isMarkdownFile, + isPreviewable, + isTextEditable, +} from './file-viewer' export { PreviewPanel, RICH_PREVIEWABLE_EXTENSIONS, resolvePreviewType } from './preview-panel' diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.test.ts new file mode 100644 index 00000000000..8bfeee15cd2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.test.ts @@ -0,0 +1,63 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { looksLikeMermaid } from './mermaid-diagram' + +describe('looksLikeMermaid', () => { + it('detects blocks whose first line opens with a diagram keyword', () => { + expect(looksLikeMermaid('flowchart TD\n A --> B')).toBe(true) + expect(looksLikeMermaid('graph LR\n A --> B')).toBe(true) + expect(looksLikeMermaid('sequenceDiagram\n Alice->>Bob: Hi')).toBe(true) + expect(looksLikeMermaid('pie title NETFLIX\n "A" : 90')).toBe(true) + expect(looksLikeMermaid('stateDiagram-v2\n [*] --> S')).toBe(true) + expect(looksLikeMermaid('gantt\n title A')).toBe(true) + }) + + it('detects the remaining diagram openers', () => { + expect(looksLikeMermaid('classDiagram\n Animal <|-- Duck')).toBe(true) + expect(looksLikeMermaid('stateDiagram\n [*] --> S')).toBe(true) + expect(looksLikeMermaid('erDiagram\n A ||--o{ B : has')).toBe(true) + expect(looksLikeMermaid('journey\n title My day')).toBe(true) + expect(looksLikeMermaid('quadrantChart\n title Reach')).toBe(true) + expect(looksLikeMermaid('requirementDiagram')).toBe(true) + expect(looksLikeMermaid('gitGraph')).toBe(true) + expect(looksLikeMermaid('mindmap\n root')).toBe(true) + expect(looksLikeMermaid('timeline\n title History')).toBe(true) + expect(looksLikeMermaid('sankey-beta')).toBe(true) + expect(looksLikeMermaid('xychart-beta')).toBe(true) + expect(looksLikeMermaid('block-beta')).toBe(true) + expect(looksLikeMermaid('packet-beta')).toBe(true) + expect(looksLikeMermaid('kanban')).toBe(true) + expect(looksLikeMermaid('architecture-beta')).toBe(true) + expect(looksLikeMermaid('zenuml')).toBe(true) + expect(looksLikeMermaid('C4Context\n title System')).toBe(true) + expect(looksLikeMermaid('C4Container')).toBe(true) + expect(looksLikeMermaid('C4Component')).toBe(true) + expect(looksLikeMermaid('C4Dynamic')).toBe(true) + expect(looksLikeMermaid('C4Deployment')).toBe(true) + }) + + it('skips leading blank lines before the opener', () => { + expect(looksLikeMermaid('\n\n flowchart TD\n A --> B')).toBe(true) + expect(looksLikeMermaid('\n \n\t\nsequenceDiagram\n Alice->>Bob: Hi')).toBe(true) + }) + + it('rejects ordinary code that merely contains a keyword later', () => { + expect(looksLikeMermaid('const graph = makeGraph()\nreturn graph')).toBe(false) + expect(looksLikeMermaid('print("pie")')).toBe(false) + expect(looksLikeMermaid('SELECT * FROM pies')).toBe(false) + expect(looksLikeMermaid('')).toBe(false) + expect(looksLikeMermaid('\n\n \n')).toBe(false) + expect(looksLikeMermaid('# flowchart of the system')).toBe(false) + expect(looksLikeMermaid(' // graph helpers')).toBe(false) + }) + + it('requires a word boundary after the keyword', () => { + expect(looksLikeMermaid('graphql query { user }')).toBe(false) + expect(looksLikeMermaid('pieChart()')).toBe(false) + expect(looksLikeMermaid('flowcharting()')).toBe(false) + expect(looksLikeMermaid('ganttify')).toBe(false) + expect(looksLikeMermaid('journeyman')).toBe(false) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2085b046840038034a2aeee43f54a8d72575a948 GIT binary patch literal 7062 zcmbtZ>vG%174C06#X6G-L02H{_%f|3O7+OHt*PXAOieQr#kH2el0*b780>-)O>!m= z(P!v`vwMJPOSt>@=QqG+1tyCSy^eeWQ8coeTKEu@mM6< zJPtM2g00!ODht-B1W#}zoY3WIRh3m(|GUbHeyy`y^#v`c*4~Mbq5`qu6x`0zU1zei zwI6a^PDRl$m*P&xnm)D3zN^GM6SwCjPcu1rRq@t|UVmm+V>RWKXjA{GEDJu$g*zUk z>GeOkmDrtyh58R4vR7H9bev~Wup6{Ay&0#&xE<{!EEM;lNCy;`rVw}lL zXQk}1hyA^zluEOU)AtuI&R%}};oa%m)AvI**ki2!uju$-oR_x=gw!h>o=sOu{8|f{ zh?kbyD#^K0{skv<4VNP>SG-E5 znFfmLO4x-@Le@pTI(d3hN)1=KCk`f+c?Jw6M8fmSh2%w+(36{6%WEn6D*Kqu4ToPQtqD(6`CSNjH%2*ULyT|KI-hrY>|B5Xm#mhWPbMN|*Z84d^!M7z!pd=AUJ03!s3cJ8u<`qE{|0yj zu~7urJey4Qt)Sm*D(WgzI!o9&Ov7TySEMyc~_N*Pzn({76fLN@B~(Z zW2{3!cKFZNYM-JR&!Yty5!?Q}5xAG-WCHmojqd<6h4@O0^Cqtr^J7YQJ@$gB zg6BDi&Y+N(DVAxQvnN$~OA6F-1esEiTF+bW!2%yoUYxu+{dj(MadrtGK7Z;mm6}gZ zjKu~_Ldgq0JG3y^cW^k0x_bdQMw#J!62Y1tFZ*oH^IG^>!lyek_w*bSSQ!L`AtelIAib_1q(o{%RS?|aB1W{a3Mza6JS94xP zactkZA38^d=6$N;T-*UwqEK;yd<+7=)JRifQyXptSnMX~KabdG;+d(s8uLWNPr4u2 z>@I%7W{dc7?^)mt?rMfS(^85uQGE6I@$Bw{Ni?36`3`AxhSWylyYzS1CkkE}#+Fgb zDQvZJPU%u|v1TMw{!zw7twq{0nx(O9vpQy-0EXXx`%j0FF9{C>I{&jRZPzaz_TgNw z#kQlV_{Ad4bXnQPXorcPZAf>BrlxJxB7W2$4q6{yYT`;m7sPE4(mN{AVu09lF&!XCI;4ree)?;Uz}A`vxB z;5PwfD6X8%c%=lYgJ}p%gG|Yo4NY_EN)U=*4x`(?l||RRqDZgJN7O3cn|4htFtsB? zVSv$ZQtDPL7Eq#0GvXqFT@#b0Q9=(SO-l@gL$vV%EqR2FZ zq1;YTZavd3J3cnM8WUWZI5Bk1tobF%M89>im4(guv!*JysYC{*t#dX&MG7mJb2i7A z_x8D+dchj4R$|IkTQ}SDk#P@r{$u*w7aDyboP@bM&yfLK25YHfq484WoB||MAr&D6A3u8p zzdo|sbG48O3yIU71M;nF8Pk}<{IgN4ZFF7Lg79d6e8zBreSn;7ZyMGRf!}g;`u0L5 z>bR9obC^2qpilrzpzRLyOlCmk`Xo$J_pH&VRQd-=mn#QNHV>;J!KkVi=VC5$WFDFi za6Lr1-2Wh<7MK9qX{1?RH)47@fYNtVB#j^6VBpC`b=-4d5s+_!#Au*Vl7^Z*&fRlf#Y-e3;HE${lPhWOj%*6m*O zm+tzy@igIms|W=$jZEar2cNN=iFFi~kWTl5s11RT<-8VKOYfa>$iRWl)|Oqa@orfo zHP9R$iK=RQ{Y!Fx5uZlhT^uoy-uG~8x4U;Wg7k17s`y0EOw)PA(U7Fnomdn2#;ca0OczQoQEyQ-=x`X_5il0Abgfe% zu5ESfVc#Xb;9hK_jMD8AVcN7+VBvvdlDPH0jWlK(6CbYuh~Pj8zya1VOEBS7o}CfI zk|GO%#nnB$@3*VP{$@rDeZdEl*~or?SQ$X(<#Qq&Uf2S#aSPFE=?3lTs(lwRSQ>k; z-BdE@Y>g`&*0*yP>0+!;q98BOkZi4Jx**etgzD?k5N&O`WpBkT(J}x1w0ZOHbRzGE z0bsBsLZNS`rqEt$OyjGk-#?<+v$@XbG@wNCr5ksi4-=l_B4nK5KP!l`{Xt3>8nHiE zdfA(!e%_>=X3S2xoCt7H`CJ<%x+KTFHG-!~ak*zFatvg7oJpV_NsbpeawEoE zbK$rRPOs9t9sh$AY(MBs<1xkx7a9Z@e{S>EZ5fU = { @@ -81,8 +44,6 @@ interface PreviewPanelProps { workspaceId: string fileKey: string isStreaming?: boolean - disableAutoScroll?: boolean - onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void /** * Read-only surface (e.g. the public share page) — disables interactive * affordances such as the CSV "Import as a table" action, which needs an @@ -98,21 +59,10 @@ export const PreviewPanel = memo(function PreviewPanel({ workspaceId, fileKey, isStreaming, - disableAutoScroll, - onCheckboxToggle, readOnly, }: PreviewPanelProps) { const previewType = resolvePreviewType(mimeType, filename) - if (previewType === 'markdown') - return ( - - ) if (previewType === 'html') return if (previewType === 'csv') return ( @@ -130,876 +80,6 @@ export const PreviewPanel = memo(function PreviewPanel({ return null }) -const CALLOUT_TYPES = new Set(['NOTE', 'TIP', 'WARNING', 'IMPORTANT', 'CAUTION']) - -function remarkMermaid() { - return (tree: { type: string; children?: unknown[] }) => { - function processNode(node: { - type: string - children?: unknown[] - lang?: string - value?: string - data?: Record - }) { - if (!node.children) return - for (const child of node.children) { - const c = child as typeof node - if (c.type === 'code' && c.lang === 'mermaid') { - c.data = { - hName: 'mermaid-diagram', - hProperties: { definition: c.value ?? '' }, - hChildren: [], - } - } else { - processNode(c) - } - } - } - processNode(tree) - } -} - -function remarkCallouts() { - return (tree: { type: string; children?: unknown[] }) => { - function processNode(node: { type: string; children?: unknown[] }) { - if (!node.children) return - for (const child of node.children) { - processNode(child as { type: string; children?: unknown[] }) - const c = child as { - type: string - children?: unknown[] - data?: { hName?: string; hProperties?: Record } - } - if (c.type !== 'blockquote') continue - const first = (c.children?.[0] ?? null) as { - type: string - children?: unknown[] - } | null - if (!first || first.type !== 'paragraph') continue - const firstText = (first.children?.[0] ?? null) as { - type: string - value?: string - } | null - if (!firstText || firstText.type !== 'text' || !firstText.value) continue - const match = firstText.value.match(/^\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]\s?/i) - if (!match) continue - const calloutType = match[1].toUpperCase() - if (!CALLOUT_TYPES.has(calloutType)) continue - - c.data ??= {} - c.data.hProperties = { ...(c.data.hProperties ?? {}), 'data-callout': calloutType } - - const remainder = firstText.value.slice(match[0].length) - if (remainder) { - firstText.value = remainder - } else if (first.children && first.children.length === 1) { - c.children?.shift() - } else { - first.children?.shift() - } - } - } - processNode(tree) - } -} - -const REMARK_PLUGINS = [remarkGfm, remarkBreaks, remarkMermaid, remarkCallouts] -const REHYPE_PLUGINS = [rehypeSlug] - -const STREAMDOWN_ALLOWED_TAGS: Record = { - 'mermaid-diagram': ['definition'], -} - -/** - * Soft per-character fade for newly revealed markdown while streaming. Mirrors - * the chat surface so a streamed file preview reveals with the same cadence; - * paired with {@link useSmoothText}, which paces the reveal itself. - */ -const STREAM_ANIMATION = { - animation: 'fadeIn', - duration: 220, - stagger: 0, - sep: 'char', -} as const - -/** - * Gates the per-character fade to streams that build the document from - * scratch. Enabling the fade over an already-rendered document, or during - * in-place rewrites (patch snapshots), replays it on text that is already - * visible, so the gate latches off for those sessions. - */ -function useStreamAnimationGate(content: string, isStreaming: boolean): boolean { - const prevIsStreamingRef = useRef(false) - const prevContentRef = useRef(content) - const animateRef = useRef(false) - - if (isStreaming !== prevIsStreamingRef.current) { - animateRef.current = isStreaming && content.length <= RESUME_SKIP_THRESHOLD - } else if ( - isStreaming && - animateRef.current && - content !== prevContentRef.current && - !content.startsWith(prevContentRef.current) - ) { - animateRef.current = false - } - prevIsStreamingRef.current = isStreaming - prevContentRef.current = content - - return isStreaming && animateRef.current -} - -/** - * Carries the contentRef and toggle handler from MarkdownPreview down to the - * task-list renderers. Only present when the preview is interactive. - */ -const MarkdownCheckboxCtx = createContext<{ - contentRef: React.MutableRefObject - onToggle: (index: number, checked: boolean) => void -} | null>(null) - -/** Carries the resolved checkbox index from LiRenderer to InputRenderer. */ -const CheckboxIndexCtx = createContext(-1) - -const NavigateCtx = createContext<((path: string) => void) | null>(null) - -const CALLOUT_CONFIG: Record< - string, - { label: string; borderColor: string; bgColor: string; textColor: string; iconColor: string } -> = { - NOTE: { - label: 'Note', - borderColor: 'border-blue-400/60', - bgColor: 'bg-blue-400/10', - textColor: 'text-[var(--text-primary)]', - iconColor: 'text-blue-500', - }, - TIP: { - label: 'Tip', - borderColor: 'border-emerald-400/60', - bgColor: 'bg-emerald-400/10', - textColor: 'text-[var(--text-primary)]', - iconColor: 'text-emerald-500', - }, - WARNING: { - label: 'Warning', - borderColor: 'border-amber-400/60', - bgColor: 'bg-amber-400/10', - textColor: 'text-[var(--text-primary)]', - iconColor: 'text-amber-500', - }, - IMPORTANT: { - label: 'Important', - borderColor: 'border-violet-400/60', - bgColor: 'bg-violet-400/10', - textColor: 'text-[var(--text-primary)]', - iconColor: 'text-violet-500', - }, - CAUTION: { - label: 'Caution', - borderColor: 'border-red-400/60', - bgColor: 'bg-red-400/10', - textColor: 'text-[var(--text-primary)]', - iconColor: 'text-red-500', - }, -} - -const CALLOUT_ICONS: Record = { - NOTE: 'ℹ', - TIP: '💡', - WARNING: '⚠', - IMPORTANT: '❕', - CAUTION: '🛑', -} - -const LANG_ALIASES: Record = { - js: 'javascript', - ts: 'typescript', - tsx: 'typescript', - jsx: 'javascript', - sh: 'bash', - shell: 'bash', - html: 'markup', - xml: 'markup', - yml: 'yaml', - py: 'python', -} - -function CalloutBlock({ type, children }: { type: string; children?: React.ReactNode }) { - const config = CALLOUT_CONFIG[type] - if (!config) { - return ( -
- {children} -
- ) - } - return ( -
-
- {CALLOUT_ICONS[type]} - {config.label} -
-
{children}
-
- ) -} - -function MermaidSourcePreview({ - definition, - isRendering, - status, -}: { - definition: string - isRendering: boolean - status?: string -}) { - return ( -
-
- mermaid - {(isRendering || status) && ( - - {isRendering ? 'Rendering…' : status} - - )} -
-
-
-          {definition}
-        
-
-
- ) -} - -function MermaidCodeBlockFallback() { - return ( -
-
- mermaid - Rendering… -
-
-
-
-
- ) -} - -const MermaidDiagram = memo(function MermaidDiagram({ - definition, - isStreaming = false, - zoomable = false, - zoomClassName, -}: { - definition: string - isStreaming?: boolean - zoomable?: boolean - zoomClassName?: string -}) { - const [svg, setSvg] = useState(null) - const [error, setError] = useState(null) - const [isRendering, setIsRendering] = useState(false) - const [renderedDefinition, setRenderedDefinition] = useState(null) - const trimmedDefinition = definition.trim() - - useEffect(() => { - if (typeof window === 'undefined') return - if (!trimmedDefinition) { - setSvg(null) - setError(null) - setIsRendering(false) - setRenderedDefinition(null) - return - } - - let cancelled = false - const renderDelay = isStreaming ? 150 : 0 - - async function render() { - try { - setIsRendering(true) - const { default: mermaid } = await import('mermaid') - if (cancelled) return - - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'strict', - theme: 'default', - }) - mermaid.setParseErrorHandler?.(() => undefined) - - if (isStreaming) { - const parsed = await mermaid.parse(trimmedDefinition, { suppressErrors: true }) - if (!parsed) { - if (!cancelled) { - setError(null) - } - return - } - } else { - await mermaid.parse(trimmedDefinition) - } - - const { svg: rendered } = await mermaid.render( - `mermaid-${generateShortId(8)}`, - trimmedDefinition - ) - if (!cancelled) { - setSvg(rendered) - setRenderedDefinition(trimmedDefinition) - setError(null) - } - } catch (err) { - if (!cancelled) { - if (isStreaming) { - setError(null) - } else { - setError(toError(err).message || 'Failed to render diagram') - setSvg(null) - setRenderedDefinition(null) - } - } - } finally { - if (!cancelled) { - setIsRendering(false) - } - } - } - - setError(null) - const timer = window.setTimeout(() => { - render() - }, renderDelay) - return () => { - cancelled = true - window.clearTimeout(timer) - } - }, [trimmedDefinition, isStreaming]) - - if (error) { - return ( - - ) - } - - if (svg && renderedDefinition === trimmedDefinition) { - const diagram =
- - if (zoomable) { - return ( - - {diagram} - - ) - } - - return ( -
- ) - } - - if (isStreaming) { - return - } - - if (!trimmedDefinition || !svg || renderedDefinition !== trimmedDefinition) { - if (zoomable) { - return - } - return - } - return null -}) - -function resolveSimFileUrl(src: string | undefined): string | undefined { - if (!src) return src - try { - const parsed = new URL(src, 'http://placeholder') - if (parsed.origin !== 'http://placeholder') return src - const [, seg1, , seg3, fileId] = parsed.pathname.split('/') - if (seg1 === 'workspace' && seg3 === 'files' && fileId) { - return `/api/files/view/${fileId}` - } - } catch { - // not a parseable URL - } - return src -} - -const STATIC_MARKDOWN_COMPONENTS = { - pre: ({ children }: { children?: React.ReactNode }) => ( - <> - {Children.map(children, (child) => - isValidElement>(child) - ? cloneElement(child, { 'data-block': 'true' }) - : child - ) ?? children} - - ), - p: ({ children }: { children?: React.ReactNode }) => ( -

- {children} -

- ), - h1: ({ id, children }: { id?: string; children?: React.ReactNode }) => ( -

- {children} -

- ), - h2: ({ id, children }: { id?: string; children?: React.ReactNode }) => ( -

- {children} -

- ), - h3: ({ id, children }: { id?: string; children?: React.ReactNode }) => ( -

- {children} -

- ), - h4: ({ id, children }: { id?: string; children?: React.ReactNode }) => ( -

- {children} -

- ), - h5: ({ id, children }: { id?: string; children?: React.ReactNode }) => ( -
- {children} -
- ), - h6: ({ id, children }: { id?: string; children?: React.ReactNode }) => ( -
- {children} -
- ), - inlineCode: ({ children }: { children?: React.ReactNode }) => ( - - {children} - - ), - code: ({ children, className }: { children?: React.ReactNode; className?: string }) => { - const langMatch = className?.match(/language-(\w+)/) - const langRaw = langMatch?.[1] ?? '' - const codeString = extractTextContent(children) - - if (!codeString) { - return ( - - {children} - - ) - } - - const resolved = LANG_ALIASES[langRaw] || langRaw || 'javascript' - const grammar = languages[resolved] || languages.javascript - const html = grammar ? highlight(codeString.trimEnd(), grammar, resolved) : null - - return ( -
-
- {langRaw || 'code'} -
-
- {html ? ( -
-          ) : (
-            
-              {codeString.trimEnd()}
-            
- )} -
-
- ) - }, - strong: ({ children }: { children?: React.ReactNode }) => ( - {children} - ), - em: ({ children }: { children?: React.ReactNode }) => {children}, - del: ({ children }: { children?: React.ReactNode }) => ( - {children} - ), - blockquote: ({ - children, - 'data-callout': calloutType, - }: { - children?: React.ReactNode - 'data-callout'?: string - }) => { - if (calloutType && CALLOUT_TYPES.has(calloutType)) { - return {children} - } - return ( -
- {children} -
- ) - }, - hr: () =>
, - img: ({ src, alt }: React.ImgHTMLAttributes) => { - const resolvedSrc = resolveSimFileUrl(typeof src === 'string' ? src : undefined) - return ( - - {alt - - ) - }, - table: ({ children }: { children?: React.ReactNode }) => ( -
- {children}
-
- ), - thead: ({ children }: { children?: React.ReactNode }) => ( - {children} - ), - tbody: ({ children }: { children?: React.ReactNode }) => {children}, - tr: ({ children }: { children?: React.ReactNode }) => ( - {children} - ), - th: ({ children }: { children?: React.ReactNode }) => ( - - {children} - - ), - td: ({ children }: { children?: React.ReactNode }) => ( - {children} - ), -} - -function UlRenderer({ className, children }: { className?: string; children?: React.ReactNode }) { - const isTaskList = typeof className === 'string' && className.includes('contains-task-list') - return ( -
    - {children} -
- ) -} - -function OlRenderer({ className, children }: { className?: string; children?: React.ReactNode }) { - const isTaskList = typeof className === 'string' && className.includes('contains-task-list') - return ( -
    - {children} -
- ) -} - -function LiRenderer({ - className, - children, - node, -}: { - className?: string - children?: React.ReactNode - node?: HastNode -}) { - const ctx = use(MarkdownCheckboxCtx) - const isTaskItem = typeof className === 'string' && className.includes('task-list-item') - - if (isTaskItem) { - const [checkboxChild, ...contentChildren] = Children.toArray(children) - const content = {contentChildren} - - if (ctx) { - const offset = node?.position?.start?.offset - if (offset === undefined) { - return ( -
  • - {checkboxChild} - {content} -
  • - ) - } - const before = ctx.contentRef.current.slice(0, offset) - const prior = before.match(/^(\s*(?:[-*+]|\d+[.)]) +)\[([ xX])\]/gm) - return ( - -
  • - {checkboxChild} - {content} -
  • -
    - ) - } - return ( -
  • - {checkboxChild} - {content} -
  • - ) - } - - return
  • {children}
  • -} - -function InputRenderer({ type, checked, ...props }: React.ComponentPropsWithoutRef<'input'>) { - const ctx = use(MarkdownCheckboxCtx) - const index = use(CheckboxIndexCtx) - - if (type !== 'checkbox') return - - const isInteractive = ctx !== null && index >= 0 - - return ( - ctx.onToggle(index, Boolean(newChecked)) : undefined - } - disabled={!isInteractive} - size='sm' - className='mt-1 shrink-0' - /> - ) -} - -function isInternalHref( - href: string, - origin = window.location.origin -): { pathname: string; hash: string } | null { - if (href.startsWith('#')) return { pathname: '', hash: href } - try { - const url = new URL(href, origin) - if (url.origin === origin && url.pathname.startsWith('/workspace/')) { - return { pathname: url.pathname, hash: url.hash } - } - } catch { - if (href.startsWith('/workspace/')) { - const hashIdx = href.indexOf('#') - if (hashIdx === -1) return { pathname: href, hash: '' } - return { pathname: href.slice(0, hashIdx), hash: href.slice(hashIdx) } - } - } - return null -} - -function AnchorRenderer({ href, children }: { href?: string; children?: React.ReactNode }) { - const navigate = use(NavigateCtx) - const parsed = href ? isInternalHref(href) : null - - const handleMarkdownLinkClick = (e: React.MouseEvent) => { - if (!parsed || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return - - e.preventDefault() - - if (parsed.pathname === '' && parsed.hash) { - const el = document.getElementById(parsed.hash.slice(1)) - if (el) { - const container = el.closest('.overflow-auto') as HTMLElement | null - if (container) { - container.scrollTo({ top: el.offsetTop - container.offsetTop, behavior: 'smooth' }) - } else { - el.scrollIntoView({ behavior: 'smooth' }) - } - } - return - } - - const destination = parsed.pathname + parsed.hash - if (navigate) { - navigate(destination) - } else { - window.location.assign(destination) - } - } - - return ( -
    - {children} - - ) -} - -const MARKDOWN_COMPONENTS = { - ...STATIC_MARKDOWN_COMPONENTS, - a: AnchorRenderer, - ul: UlRenderer, - ol: OlRenderer, - li: LiRenderer, - input: InputRenderer, -} - -function createMarkdownComponents(isStreaming: boolean) { - return { - ...MARKDOWN_COMPONENTS, - 'mermaid-diagram': ({ definition }: { definition?: string }) => ( - - ), - } -} - -function FrontMatterCard({ data }: { data: Record }) { - const entries = Object.entries(data) - if (entries.length === 0) return null - - return ( -
    -
    - {entries.map(([key, value]) => ( -
    -
    {key}:
    -
    - {Array.isArray(value) - ? value.join(', ') - : value instanceof Date - ? value.toISOString().split('T')[0] - : String(value ?? '')} -
    -
    - ))} -
    -
    - ) -} - -const MarkdownPreview = memo(function MarkdownPreview({ - content, - isStreaming = false, - disableAutoScroll = false, - onCheckboxToggle, -}: { - content: string - isStreaming?: boolean - disableAutoScroll?: boolean - onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void -}) { - const { push: navigate } = useRouter() - // Pace the reveal so streamed markdown builds at a steady cadence instead of - // jumping per server chunk. `snapOnNonAppend` shows in-place rewrites (patch) - // in full immediately so a diff never appears to retype from the top. - const revealedContent = useSmoothText(content, isStreaming, { snapOnNonAppend: true }) - const shouldAnimateStream = useStreamAnimationGate(content, isStreaming) - const { ref: autoScrollRef, spacerRef } = useScrollAnchor( - isStreaming && !disableAutoScroll, - revealedContent - ) - - const contentRef = useRef(content) - contentRef.current = content - - const { frontMatterData, markdownContent } = useMemo(() => { - if (isStreaming) return { frontMatterData: null, markdownContent: revealedContent } - try { - const parsed = matter(content) - const hasFrontMatter = Object.keys(parsed.data).length > 0 - return { - frontMatterData: hasFrontMatter ? parsed.data : null, - markdownContent: hasFrontMatter ? parsed.content : content, - } - } catch { - return { frontMatterData: null, markdownContent: content } - } - }, [content, revealedContent, isStreaming]) - - const ctxValue = useMemo( - () => (onCheckboxToggle ? { contentRef, onToggle: onCheckboxToggle } : null), - [onCheckboxToggle] - ) - - const hasScrolledToHash = useRef(false) - useEffect(() => { - const hash = window.location.hash - if (!hash || hasScrolledToHash.current) return - const id = hash.slice(1) - const el = document.getElementById(id) - if (!el) return - hasScrolledToHash.current = true - const container = el.closest('.overflow-auto') as HTMLElement | null - if (container) { - container.scrollTo({ top: el.offsetTop - container.offsetTop, behavior: 'smooth' }) - } else { - el.scrollIntoView({ behavior: 'smooth' }) - } - }, [content]) - - const streamdownMode = isStreaming ? undefined : 'static' - const markdownComponents = useMemo(() => createMarkdownComponents(isStreaming), [isStreaming]) - - const body = ( -
    - {frontMatterData && } - - {markdownContent} - -
    -
    - ) - - return ( - - {body} - - ) -}) - const HTML_PREVIEW_BASE_URL = 'about:srcdoc' const HTML_PREVIEW_CSP = [ diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx new file mode 100644 index 00000000000..54ad3d9624c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx @@ -0,0 +1,262 @@ +import { useEffect, useState } from 'react' +import type { JSONContent } from '@tiptap/core' +import { CodeBlock } from '@tiptap/extension-code-block' +import type { ReactNodeViewProps } from '@tiptap/react' +import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' +import { Check, ChevronDown, Code, Copy, Eye, WrapText } from 'lucide-react' +import { + chipVariants, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' +import { looksLikeMermaid, MermaidDiagram } from '../mermaid-diagram' +import { detectLanguage } from './detect-language' + +const PLAIN = 'plain' +const MERMAID = 'mermaid' + +/** Languages the Prism highlighter has registered (see {@link CodeBlockHighlight}). Every non-plain + * value MUST have a grammar registered in {@link CodeBlockHighlight} — enforced by a unit test. */ +export const LANGUAGE_OPTIONS = [ + { value: PLAIN, label: 'Plain text' }, + { value: 'bash', label: 'Bash' }, + { value: 'c', label: 'C' }, + { value: 'cpp', label: 'C++' }, + { value: 'csharp', label: 'C#' }, + { value: 'css', label: 'CSS' }, + { value: 'go', label: 'Go' }, + { value: 'java', label: 'Java' }, + { value: 'javascript', label: 'JavaScript' }, + { value: 'json', label: 'JSON' }, + { value: 'markup', label: 'HTML' }, + { value: 'php', label: 'PHP' }, + { value: 'python', label: 'Python' }, + { value: 'ruby', label: 'Ruby' }, + { value: 'rust', label: 'Rust' }, + { value: 'sql', label: 'SQL' }, + { value: 'typescript', label: 'TypeScript' }, + { value: 'yaml', label: 'YAML' }, +] as const + +const CONTROL_CLASS = + 'flex size-[24px] items-center justify-center rounded-lg text-[var(--text-icon)] outline-none transition-colors hover-hover:bg-[var(--surface-hover)] hover-hover:text-[var(--text-body)] focus-visible:bg-[var(--surface-hover)] [&_svg]:size-[14px]' + +/** + * Code block view with hover controls (language picker, line-wrap, copy). When the block holds + * Mermaid — tagged ```mermaid or {@link looksLikeMermaid auto-detected} — it renders as a diagram + * whenever the cursor is outside it (and always in read-only), and as editable source while the + * cursor is inside, re-rendering on blur (the Linear/GitHub model). The source `
    ` stays mounted
    + * (hidden behind the diagram) so ProseMirror keeps managing its contentDOM, and the node remains an
    + * ordinary code block, so markdown round-trips unchanged.
    + */
    +function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeViewProps) {
    +  const [wrap, setWrap] = useState(false)
    +  const [menuOpen, setMenuOpen] = useState(false)
    +  const [editingInline, setEditingInline] = useState(false)
    +  const [peekSource, setPeekSource] = useState(false)
    +  const { copied, copy } = useCopyToClipboard({ resetMs: 1500 })
    +
    +  const explicitLanguage = node.attrs.language as string | null
    +  const text = node.textContent
    +  const isMermaid = explicitLanguage === MERMAID || (!explicitLanguage && looksLikeMermaid(text))
    +
    +  // Editable Mermaid shows source while the caret is focused inside the block and re-renders the
    +  // diagram on blur (the Linear/GitHub model). The Show source / Show diagram control drives this by
    +  // focusing into / blurring the block; read-only uses {@link peekSource} since there is no caret.
    +  useEffect(() => {
    +    if (!isMermaid || !editor.isEditable) {
    +      setEditingInline(false)
    +      return
    +    }
    +    const sync = () => {
    +      const pos = typeof getPos === 'function' ? getPos() : null
    +      if (typeof pos !== 'number') {
    +        setEditingInline(false)
    +        return
    +      }
    +      const size = editor.state.doc.nodeAt(pos)?.nodeSize ?? 0
    +      const { from } = editor.state.selection
    +      setEditingInline(editor.isFocused && from > pos && from < pos + size)
    +    }
    +    sync()
    +    editor.on('selectionUpdate', sync)
    +    editor.on('focus', sync)
    +    editor.on('blur', sync)
    +    return () => {
    +      editor.off('selectionUpdate', sync)
    +      editor.off('focus', sync)
    +      editor.off('blur', sync)
    +    }
    +  }, [editor, getPos, isMermaid])
    +
    +  const showSource = editor.isEditable ? editingInline : peekSource
    +  const showDiagram = isMermaid && text.trim().length > 0 && !showSource
    +
    +  // Skip language detection on the mermaid path — the picker/label never render there.
    +  const language = explicitLanguage ?? (isMermaid ? null : detectLanguage(text)) ?? PLAIN
    +  const label =
    +    LANGUAGE_OPTIONS.find((option) => option.value === language)?.label ??
    +    explicitLanguage ??
    +    'Plain text'
    +
    +  const toggleSource = () => {
    +    if (!editor.isEditable) {
    +      setPeekSource((value) => !value)
    +      return
    +    }
    +    const pos = typeof getPos === 'function' ? getPos() : null
    +    if (typeof pos !== 'number') return
    +    if (editingInline) {
    +      // Back to the diagram: select the whole node (reliable, and shows the same ring) rather than
    +      // relying on a blur event to fire.
    +      editor.commands.setNodeSelection(pos)
    +      return
    +    }
    +    editor
    +      .chain()
    +      .focus()
    +      .setTextSelection(pos + 1)
    +      .run()
    +  }
    +
    +  return (
    +    
    +      
    + {isMermaid && ( + + )} + {!isMermaid && + (editor.isEditable ? ( + // Editable: a language picker. Read-only: a static label — selecting a language calls + // updateAttributes, which would mutate a doc that must not change. + + + + + + {LANGUAGE_OPTIONS.map((option) => ( + + updateAttributes({ language: option.value === PLAIN ? null : option.value }) + } + > + {option.label} + + ))} + + + ) : ( + + {label} + + ))} + {!isMermaid && ( + + )} + +
    +
    +         as='code' />
    +      
    + {showDiagram && ( + // Clicking the diagram selects the whole node (same selection ring as an image/code block) + // instead of dropping a caret inside — preventDefault stops ProseMirror placing the caret, + // which would otherwise flip to source. Editing is an explicit Show source / blur action. +
    { + event.preventDefault() + const pos = typeof getPos === 'function' ? getPos() : null + if (typeof pos === 'number') editor.commands.setNodeSelection(pos) + }} + > + +
    + )} +
    + ) +} + +function codeBlockText(node: JSONContent): string { + return (node.content ?? []).map((child) => child.text ?? '').join('') +} + +/** Fence sized to one backtick longer than the longest run inside the code (CommonMark rule). */ +function fenceFor(text: string): string { + const longestRun = Math.max(0, ...[...text.matchAll(/`+/g)].map((match) => match[0].length)) + return '`'.repeat(Math.max(3, longestRun + 1)) +} + +/** + * Code block whose markdown serializer sizes the fence to the interior backtick runs, so a code + * block that itself contains a ``` line round-trips instead of shattering. Shared by the test + * (plain) and live ({@link CodeBlockWithLanguage}) paths. + */ +export const MarkdownCodeBlock = CodeBlock.extend({ + renderMarkdown: (node: JSONContent) => { + const language = typeof node.attrs?.language === 'string' ? node.attrs.language : '' + const text = codeBlockText(node) + const fence = fenceFor(text) + return `${fence}${language}\n${text}\n${fence}` + }, +}) + +/** + * Code block with hover-revealed controls (language picker, line-wrap toggle, copy). The + * `language` attribute drives {@link CodeBlockHighlight}'s Prism highlighting and serializes to + * the ```lang fence on save; wrap is a view-only preference. + */ +export const CodeBlockWithLanguage = MarkdownCodeBlock.extend({ + addNodeView() { + return ReactNodeViewRenderer(CodeBlockView) + }, +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.test.ts new file mode 100644 index 00000000000..6b74e26da37 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.test.ts @@ -0,0 +1,81 @@ +/** + * @vitest-environment jsdom + */ +import { Editor } from '@tiptap/core' +import { afterEach, describe, expect, it } from 'vitest' +import { buildDecorations, changeTouchesCodeBlock } from './code-highlight' +import { createMarkdownContentExtensions } from './extensions' + +let editor: Editor | null = null + +/** Position just inside the first code block in the current editor doc. */ +function codeBlockPos(ed: Editor): number { + let pos = -1 + ed.state.doc.descendants((node, p) => { + if (pos === -1 && node.type.name === 'codeBlock') pos = p + return pos === -1 + }) + if (pos === -1) throw new Error('no code block') + return pos +} + +function decorationClassesFor(markdown: string): string[] { + editor = new Editor({ extensions: createMarkdownContentExtensions() }) + editor.commands.setContent(markdown, { contentType: 'markdown' }) + const decorations = buildDecorations(editor.state.doc).find() + editor.destroy() + editor = null + return decorations.map( + (decoration) => + (decoration as unknown as { type: { attrs: { class: string } } }).type.attrs.class + ) +} + +afterEach(() => { + editor?.destroy() + editor = null +}) + +describe('code block syntax highlighting', () => { + it('emits Prism token decorations for a known language', () => { + const classes = decorationClassesFor('```js\nconst x = 1\n```') + expect(classes.length).toBeGreaterThan(0) + expect(classes.every((c) => c.startsWith('token'))).toBe(true) + expect(classes.some((c) => c.includes('keyword'))).toBe(true) + }) + + it('does not decorate plain prose', () => { + expect(decorationClassesFor('just some text')).toHaveLength(0) + }) + + it('does not decorate an unregistered language', () => { + expect(decorationClassesFor('```unregistered-lang\n+++ foo\n```')).toHaveLength(0) + }) +}) + +describe('changeTouchesCodeBlock (incremental re-tokenization gate)', () => { + function mount(markdown: string): Editor { + editor = new Editor({ extensions: createMarkdownContentExtensions() }) + editor.commands.setContent(markdown, { contentType: 'markdown' }) + return editor + } + + it('is false when an edit lands only in prose (decorations are mapped, not rebuilt)', () => { + const ed = mount('intro text\n\n```js\nconst x = 1\n```') + const tr = ed.state.tr.insertText('Z', 1) // inside the leading paragraph + expect(changeTouchesCodeBlock(tr, tr.doc)).toBe(false) + }) + + it('is true when an edit lands inside a code block (forces a re-tokenize)', () => { + const ed = mount('intro\n\n```js\nconst x = 1\n```') + const tr = ed.state.tr.insertText('y', codeBlockPos(ed) + 1) + expect(changeTouchesCodeBlock(tr, tr.doc)).toBe(true) + }) + + it('is true when the code block language changes via setNodeMarkup', () => { + const ed = mount('```js\nconst x = 1\n```') + const pos = codeBlockPos(ed) + const tr = ed.state.tr.setNodeMarkup(pos, undefined, { language: 'python' }) + expect(changeTouchesCodeBlock(tr, tr.doc)).toBe(true) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.ts new file mode 100644 index 00000000000..5609f56922b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-highlight.ts @@ -0,0 +1,133 @@ +import { Extension } from '@tiptap/core' +import type { Node as ProseMirrorNode } from '@tiptap/pm/model' +import { Plugin, PluginKey, type Transaction } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' +import Prism, { type Token, type TokenStream } from 'prismjs' +import 'prismjs/components/prism-bash' +import 'prismjs/components/prism-css' +import 'prismjs/components/prism-markup' +import 'prismjs/components/prism-javascript' +import 'prismjs/components/prism-typescript' +import 'prismjs/components/prism-yaml' +import 'prismjs/components/prism-sql' +import 'prismjs/components/prism-python' +import 'prismjs/components/prism-json' +import 'prismjs/components/prism-c' +import 'prismjs/components/prism-cpp' +import 'prismjs/components/prism-csharp' +import 'prismjs/components/prism-go' +import 'prismjs/components/prism-java' +import 'prismjs/components/prism-markup-templating' +import 'prismjs/components/prism-php' +import 'prismjs/components/prism-ruby' +import 'prismjs/components/prism-rust' +import { detectLanguage } from './detect-language' + +const HIGHLIGHT_PLUGIN_KEY = new PluginKey('codeBlockHighlight') + +function tokenClasses(token: Token): string { + const classes = ['token', token.type] + if (token.alias) classes.push(...(Array.isArray(token.alias) ? token.alias : [token.alias])) + return classes.join(' ') +} + +/** + * Walks Prism's token tree, emitting one inline decoration per token over its text range. + * Nested tokens stack (ProseMirror nests overlapping inline decorations), reproducing the + * `.token`-class structure Prism would render as HTML. + */ +function collectTokenDecorations( + stream: TokenStream, + base: number, + offset: { value: number }, + decorations: Decoration[], + limit: number +) { + const tokens = Array.isArray(stream) ? stream : [stream] + for (const token of tokens) { + if (typeof token === 'string') { + offset.value += token.length + continue + } + const start = offset.value + collectTokenDecorations(token.content, base, offset, decorations, limit) + const from = base + start + const to = Math.min(base + offset.value, limit) + if (to > from) decorations.push(Decoration.inline(from, to, { class: tokenClasses(token) })) + } +} + +export function buildDecorations(doc: ProseMirrorNode): DecorationSet { + const decorations: Decoration[] = [] + doc.descendants((node, pos) => { + if (node.type.name !== 'codeBlock') return + const language = (node.attrs.language as string | null) ?? detectLanguage(node.textContent) + const grammar = language ? Prism.languages[language] : undefined + if (!grammar) return + // Defensive: a malformed grammar or a token/position mismatch must never throw here — a throw + // in the decorations plugin blanks the whole editor. The `limit` clamps any over-long token. + try { + const base = pos + 1 + collectTokenDecorations( + Prism.tokenize(node.textContent, grammar), + base, + { value: 0 }, + decorations, + base + node.content.size + ) + } catch {} + }) + return DecorationSet.create(doc, decorations) +} + +/** + * Whether the transaction's changed ranges intersect any code block in the new doc — including + * a `setNodeMarkup` language change (whose step range covers the node). When false, the cheap + * path just maps existing decorations instead of re-tokenizing. + */ +export function changeTouchesCodeBlock(tr: Transaction, doc: ProseMirrorNode): boolean { + let touches = false + for (const map of tr.mapping.maps) { + map.forEach((_oldStart, _oldEnd, newStart, newEnd) => { + if (touches) return + const from = Math.max(0, Math.min(newStart, doc.content.size)) + const to = Math.max(from, Math.min(newEnd, doc.content.size)) + doc.nodesBetween(from, to, (node) => { + if (node.type.name === 'codeBlock') touches = true + return !touches + }) + }) + } + return touches +} + +/** + * Syntax-highlights fenced code blocks with Prism, emitting the same `.token` classes the + * rest of the app uses so the `code-editor-theme` styles (light + dark) apply unchanged. + * Re-tokenizes only when a change actually touches a code block (typing in prose just maps + * the existing decorations), keeping the cost off the common keystroke path. + */ +export const CodeBlockHighlight = Extension.create({ + name: 'codeBlockHighlight', + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: HIGHLIGHT_PLUGIN_KEY, + state: { + init: (_, { doc }) => buildDecorations(doc), + apply: (tr, current) => { + if (tr.steps.length === 0) return current + if (!changeTouchesCodeBlock(tr, tr.doc)) return current.map(tr.mapping, tr.doc) + return buildDecorations(tr.doc) + }, + }, + props: { + decorations(state) { + return HIGHLIGHT_PLUGIN_KEY.getState(state) + }, + }, + }), + ] + }, +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-languages.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-languages.test.ts new file mode 100644 index 00000000000..d3f830e2ee8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-languages.test.ts @@ -0,0 +1,21 @@ +/** + * @vitest-environment jsdom + * + * Guards against drift between the code-block language picker and the Prism grammars actually + * registered by CodeBlockHighlight: every selectable language must have a registered grammar, or it + * would silently fall back to no highlighting. + */ +import Prism from 'prismjs' +import { describe, expect, it } from 'vitest' +import { LANGUAGE_OPTIONS } from './code-block' +// Importing the highlighter registers all the prism-* grammars as a side effect. +import './code-highlight' + +describe('code-block languages', () => { + it('every selectable language has a registered Prism grammar', () => { + for (const { value } of LANGUAGE_OPTIONS) { + if (value === 'plain') continue + expect(Prism.languages[value], `no Prism grammar registered for "${value}"`).toBeDefined() + } + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.test.ts new file mode 100644 index 00000000000..a5c9194a7f8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { detectLanguage } from './detect-language' + +describe('detectLanguage', () => { + it('returns null for empty or unrecognizable content', () => { + expect(detectLanguage('')).toBeNull() + expect(detectLanguage(' \n ')).toBeNull() + expect(detectLanguage('just some prose words here')).toBeNull() + }) + + it('detects common languages from content shape', () => { + expect(detectLanguage('{\n "a": 1,\n "b": [2, 3]\n}')).toBe('json') + expect(detectLanguage('const x = 1\nfunction go() {}')).toBe('javascript') + expect(detectLanguage('interface Foo { name: string }')).toBe('typescript') + expect(detectLanguage('def main():\n print("hi")')).toBe('python') + expect(detectLanguage('SELECT id FROM users WHERE id = 1')).toBe('sql') + expect(detectLanguage('#!/bin/bash\necho hello')).toBe('bash') + expect(detectLanguage('
    hi
    ')).toBe('markup') + expect(detectLanguage('.btn { color: red; padding: 4px }')).toBe('css') + }) + + it('does not misclassify a JS object as JSON', () => { + expect(detectLanguage('const x = { a: 1 }')).toBe('javascript') + }) + + it('detects Go, Rust, Java', () => { + expect(detectLanguage('package main\n\nfunc main() {\n\tfmt.Println("hi")\n}')).toBe('go') + expect(detectLanguage('type User struct {\n\tName string\n}')).toBe('go') + expect(detectLanguage('fn main() {\n let mut x = 1;\n println!("{}", x);\n}')).toBe('rust') + expect(detectLanguage('public class Box {\n private int n;\n}')).toBe('java') + }) + + it('does not misread generics as HTML markup', () => { + expect(detectLanguage('public class Box { private List items; }')).toBe('java') + expect(detectLanguage('let v: Vec = Vec::new();\nfn f() {}')).toBe('rust') + expect(detectLanguage('func Map[T any](s []T) {}\npackage x')).toBe('go') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.ts new file mode 100644 index 00000000000..d391ed13d29 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/detect-language.ts @@ -0,0 +1,63 @@ +/** + * Heuristic language detection for a fenced code block that has no explicit ` ```lang ` tag. + * Used only to drive syntax highlighting + the picker label — the detected value is NEVER + * written back to the markdown, so opening a file never mutates it. Restricted to the grammars + * {@link CodeBlockHighlight} actually registers with Prism; returns `null` when unsure. + */ +const DETECTORS: ReadonlyArray<{ language: string; test: RegExp }> = [ + // Real HTML: a closing tag, an opening tag with an attribute, or a doctype/comment. Deliberately + // NOT a bare `` so generics (`List`, `Vec`) aren't misread as markup. + { language: 'markup', test: /<\/[a-z][\w-]*\s*>|<[a-z][\w-]*\s+[\w:-]+=||console\.\w+|\brequire\(|\bexport\s+(default|const)\b/, + }, + { language: 'css', test: /[.#]?[\w-]+\s*\{[^}]*[\w-]+\s*:[^};]+;?[^}]*\}/ }, + { language: 'yaml', test: /^[\w-]+:\s+\S/m }, +] + +function looksLikeJson(sample: string): boolean { + const trimmed = sample.trim() + if (!/^[[{]/.test(trimmed)) return false + try { + JSON.parse(trimmed) + return true + } catch { + return false + } +} + +export function detectLanguage(code: string): string | null { + const sample = code.slice(0, 2000) + if (!sample.trim()) return null + if (looksLikeJson(sample)) return 'json' + for (const { language, test } of DETECTORS) { + if (test.test(sample)) return language + } + return null +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/dirty-signal.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/dirty-signal.test.ts new file mode 100644 index 00000000000..870907a9a38 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/dirty-signal.test.ts @@ -0,0 +1,55 @@ +/** + * @vitest-environment jsdom + * + * The rich editor uses TipTap's initial-content model: opening a file loads its markdown as the + * editor's initial `content`, which must NOT emit an update — so a freshly opened file is never + * marked dirty (no spurious autosave / "unsaved changes"). Only a genuine edit emits, which is what + * flips the dirty/autosave state on. These two cases guard exactly that contract. + */ +import { Editor } from '@tiptap/core' +import { afterEach, describe, expect, it } from 'vitest' +import { createMarkdownContentExtensions } from './extensions' + +let editor: Editor | null = null +afterEach(() => { + editor?.destroy() + editor = null +}) + +function mount(content: string, onUpdate: () => void): Editor { + return new Editor({ + extensions: createMarkdownContentExtensions(), + content, + contentType: 'markdown', + onUpdate, + }) +} + +describe('rich markdown editor — dirty signal', () => { + it('opening a file emits no update (never dirty on open), including markdown that normalizes', () => { + // A trailing newline and `_emphasis_` both normalize on serialization; opening must still be clean. + let updates = 0 + editor = mount('# Title\n\nsome _emphasis_ here\n', () => { + updates++ + }) + expect(updates).toBe(0) + expect(editor.isEmpty).toBe(false) + }) + + it('opening an empty file emits no update and is editable', () => { + let updates = 0 + editor = mount('', () => { + updates++ + }) + expect(updates).toBe(0) + }) + + it('a genuine edit emits an update (marks dirty → triggers autosave)', () => { + let updates = 0 + editor = mount('hello', () => { + updates++ + }) + editor.commands.insertContent(' world') + expect(updates).toBeGreaterThan(0) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts new file mode 100644 index 00000000000..21921981774 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts @@ -0,0 +1,113 @@ +import type { Extensions, JSONContent, MarkdownRendererHelpers } from '@tiptap/core' +import { Code } from '@tiptap/extension-code' +import { TaskItem, TaskList } from '@tiptap/extension-list' +import Placeholder from '@tiptap/extension-placeholder' +import { + renderTableToMarkdown, + Table, + TableCell, + TableHeader, + TableRow, +} from '@tiptap/extension-table' +import { Markdown } from '@tiptap/markdown' +import StarterKit from '@tiptap/starter-kit' +import { CodeBlockWithLanguage, MarkdownCodeBlock } from './code-block' +import { CodeBlockHighlight } from './code-highlight' +import { MarkdownImage, ResizableImage } from './image' +import { RichMarkdownKeymap } from './keymap' +import { MarkdownLinkInputRule } from './link-input-rule' +import { MarkdownPaste } from './markdown-paste' +import { SlashCommand } from './slash-command/slash-command' + +/** + * Inline code that can combine with bold/italic/strike (GFM permits `**`x`**`, `~~`x`~~`). + * The stock Code mark sets `excludes: '_'`, which blocks every other mark from coexisting and + * makes the bubble-menu toggles silently no-op over a code selection. + */ +const InlineCode = Code.extend({ excludes: '' }) + +/** + * Table that escapes interior `|` characters when serializing cells. The upstream serializer + * joins cells with `|` without escaping, so a cell containing a literal pipe silently splits + * into phantom columns on round-trip (data loss). Escaping must happen on the `table` node — + * `tableCell`/`tableHeader` have no markdown renderer; the table renders cell children directly. + * + * The upstream serializer also wraps the table in its own leading/trailing blank lines; left in, + * the block joiner adds another, so an interior table churns its surrounding whitespace to + * `\n\n\n` on the first edit. Trimming the table's own output lets the joiner own the single + * blank-line separator — without touching blank lines inside fenced code (those live in the code + * node's text, not here). + */ +const PipeSafeTable = Table.extend({ + renderMarkdown: (node: JSONContent, h: MarkdownRendererHelpers) => + renderTableToMarkdown(node, { + ...h, + renderChildren: (nodes, separator) => + h.renderChildren(nodes, separator).replace(/\|/g, '\\|'), + }) + .replace(/^\n+/, '') + .replace(/\n+$/, ''), +}) + +interface MarkdownEditorExtensionOptions { + placeholder: string +} + +interface ContentExtensionOptions { + /** Use the React node views (code-block language picker, image resize). Off for headless tests. */ + nodeViews?: boolean +} + +/** + * The schema + serialization extensions: the nodes/marks the document can contain and the + * Markdown ⇄ ProseMirror conversion. `StarterKit` provides core nodes/marks and the + * Markdown-style input rules (`# `, `- `, `**bold**`, …); `TaskList`/`TaskItem` add + * `- [ ]` checklists; `TableKit` adds GFM tables; `Markdown` serializes back to markdown. + * + * The code block is the standalone `CodeBlock` so the live editor can swap in a node view; + * the schema and markdown output are identical either way. + */ +export function createMarkdownContentExtensions({ + nodeViews = false, +}: ContentExtensionOptions = {}): Extensions { + const codeBlock = (nodeViews ? CodeBlockWithLanguage : MarkdownCodeBlock).configure({ + HTMLAttributes: { class: 'code-editor-theme' }, + }) + return [ + StarterKit.configure({ + link: { openOnClick: false }, + underline: false, + codeBlock: false, + code: false, + }), + InlineCode, + codeBlock, + (nodeViews ? ResizableImage : MarkdownImage).configure({ allowBase64: true }), + TaskList, + TaskItem.configure({ nested: true }), + PipeSafeTable.configure({ resizable: true }), + TableRow, + TableHeader, + TableCell, + MarkdownLinkInputRule, + Markdown, + ] +} + +/** + * The full extension set for the live editor: the content extensions plus the UI-only + * extensions — `CodeBlockHighlight` (Prism), `SlashCommand` (the `/` block menu), and + * `Placeholder`. + */ +export function createMarkdownEditorExtensions({ + placeholder, +}: MarkdownEditorExtensionOptions): Extensions { + return [ + ...createMarkdownContentExtensions({ nodeViews: true }), + CodeBlockHighlight, + SlashCommand, + RichMarkdownKeymap, + MarkdownPaste, + Placeholder.configure({ placeholder }), + ] +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.test.ts new file mode 100644 index 00000000000..45a0cb92ae6 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.test.ts @@ -0,0 +1,57 @@ +/** + * @vitest-environment jsdom + */ +import { Editor } from '@tiptap/core' +import { afterEach, describe, expect, it } from 'vitest' +import { createMarkdownContentExtensions } from './extensions' +import { findHeadingPos, slugifyHeading } from './heading-anchors' + +let editor: Editor | null = null +afterEach(() => { + editor?.destroy() + editor = null +}) + +/** A ProseMirror doc parsed from markdown, for the position-resolution tests. */ +function docOf(markdown: string) { + editor = new Editor({ extensions: createMarkdownContentExtensions() }) + editor.commands.setContent(markdown, { contentType: 'markdown' }) + return editor.state.doc +} + +describe('slugifyHeading', () => { + it('lowercases, drops punctuation, and hyphenates whitespace (GitHub-style)', () => { + expect(slugifyHeading('Getting Started')).toBe('getting-started') + expect(slugifyHeading('API Reference!')).toBe('api-reference') + expect(slugifyHeading(' Spaced Out ')).toBe('spaced-out') + expect(slugifyHeading('Node.js & Bun')).toBe('nodejs-bun') + }) + + it('returns an empty string for punctuation-only text', () => { + expect(slugifyHeading('!!!')).toBe('') + expect(slugifyHeading('')).toBe('') + }) +}) + +describe('findHeadingPos', () => { + it('resolves a fragment slug to its heading position', () => { + const doc = docOf('# Intro\n\ntext\n\n## Getting Started\n\nmore') + expect(findHeadingPos(doc, 'intro')).toBeGreaterThanOrEqual(0) + expect(findHeadingPos(doc, 'getting-started')).toBeGreaterThan(findHeadingPos(doc, 'intro')) + }) + + it('disambiguates duplicate slugs GitHub-style (foo, foo-1, foo-2)', () => { + const doc = docOf('# Notes\n\na\n\n# Notes\n\nb\n\n# Notes\n\nc') + const first = findHeadingPos(doc, 'notes') + const second = findHeadingPos(doc, 'notes-1') + const third = findHeadingPos(doc, 'notes-2') + expect(first).toBeGreaterThanOrEqual(0) + expect(second).toBeGreaterThan(first) + expect(third).toBeGreaterThan(second) + }) + + it('returns -1 when no heading matches', () => { + const doc = docOf('# Only Heading\n\nbody') + expect(findHeadingPos(doc, 'missing')).toBe(-1) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.ts new file mode 100644 index 00000000000..677964d65e4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/heading-anchors.ts @@ -0,0 +1,36 @@ +import type { Node as ProseMirrorNode } from '@tiptap/pm/model' + +/** + * Slugify heading text GitHub-style (lowercase, drop punctuation, collapse whitespace to hyphens) so + * that `[label](#slug)` fragment links — written against how GitHub renders the same markdown — + * resolve to the matching heading. Mirrors what `rehype-slug` produced in the old preview. + */ +export function slugifyHeading(text: string): string { + return text + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') +} + +/** + * The document position of the heading a `#slug` fragment link targets, or -1 if none matches. + * Computed on demand (at click time) rather than maintained as per-keystroke decorations. Duplicate + * slugs are disambiguated GitHub-style: `intro`, `intro-1`, `intro-2`, … + */ +export function findHeadingPos(doc: ProseMirrorNode, slug: string): number { + const seen = new Map() + let found = -1 + doc.descendants((node, pos) => { + if (found >= 0) return false + if (node.type.name !== 'heading') return true + const base = slugifyHeading(node.textContent) + if (!base) return true + const n = seen.get(base) ?? 0 + seen.set(base, n + 1) + if ((n === 0 ? base : `${base}-${n}`) === slug) found = pos + return found < 0 + }) + return found +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.test.ts new file mode 100644 index 00000000000..766e4c77ef6 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.test.ts @@ -0,0 +1,56 @@ +/** + * @vitest-environment jsdom + */ +import { describe, expect, it } from 'vitest' +import { extractImageFiles } from './image-paste' + +function imageFile(name = 'shot.png'): File { + return new File([''], name, { type: 'image/png' }) +} + +function transfer( + files: File[], + items: Array<{ kind: string; type: string; file: File | null }> = [] +): DataTransfer { + return { + files, + items: items.map((entry) => ({ + kind: entry.kind, + type: entry.type, + getAsFile: () => entry.file, + })), + } as unknown as DataTransfer +} + +describe('extractImageFiles', () => { + it('returns nothing for a null payload or non-image files', () => { + expect(extractImageFiles(null)).toEqual([]) + expect(extractImageFiles(transfer([new File([''], 'a.txt', { type: 'text/plain' })]))).toEqual( + [] + ) + }) + + it('reads images from the files list (drag-drop)', () => { + const file = imageFile() + expect(extractImageFiles(transfer([file]))).toEqual([file]) + }) + + it('falls back to items when files is empty (pasted screenshot)', () => { + const file = imageFile() + const result = extractImageFiles(transfer([], [{ kind: 'file', type: 'image/png', file }])) + expect(result).toEqual([file]) + }) + + it('ignores non-file and non-image items', () => { + const result = extractImageFiles( + transfer( + [], + [ + { kind: 'string', type: 'text/plain', file: null }, + { kind: 'file', type: 'application/pdf', file: new File([''], 'a.pdf') }, + ] + ) + ) + expect(result).toEqual([]) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.ts new file mode 100644 index 00000000000..ff72fededf9 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-paste.ts @@ -0,0 +1,14 @@ +/** + * Extract image `File` objects from a paste/drop payload. Reads `files` first, then falls back to + * `items` — many browsers expose a pasted or copied image (e.g. a screenshot) only through + * `DataTransfer.items` with an empty `files` list, so reading `files` alone misses them. + */ +export function extractImageFiles(transfer: DataTransfer | null): File[] { + if (!transfer) return [] + const fromFiles = Array.from(transfer.files).filter((file) => file.type.startsWith('image/')) + if (fromFiles.length > 0) return fromFiles + return Array.from(transfer.items) + .filter((item) => item.kind === 'file' && item.type.startsWith('image/')) + .map((item) => item.getAsFile()) + .filter((file): file is File => file !== null) +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.test.ts new file mode 100644 index 00000000000..41e2f888408 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.test.ts @@ -0,0 +1,27 @@ +/** + * @vitest-environment jsdom + */ +import { describe, expect, it } from 'vitest' +import { resolveDisplaySrc } from './image' + +describe('resolveDisplaySrc', () => { + it('rewrites an in-app workspace file path to its serving endpoint (display only)', () => { + expect(resolveDisplaySrc('/workspace/W1/files/F123')).toBe('/api/files/view/F123') + expect(resolveDisplaySrc('/workspace/any-ws-id/files/abc-def')).toBe('/api/files/view/abc-def') + }) + + it('leaves absolute and non-workspace URLs untouched', () => { + expect(resolveDisplaySrc('https://cdn.example.com/a.png')).toBe('https://cdn.example.com/a.png') + expect(resolveDisplaySrc('http://localhost/workspace/W1/files/F1')).toBe( + 'http://localhost/workspace/W1/files/F1' + ) + expect(resolveDisplaySrc('/other/path/files/x')).toBe('/other/path/files/x') + expect(resolveDisplaySrc('relative/image.png')).toBe('relative/image.png') + }) + + it('passes through empty/undefined and unparseable values', () => { + expect(resolveDisplaySrc(undefined)).toBeUndefined() + expect(resolveDisplaySrc('')).toBe('') + expect(resolveDisplaySrc('/workspace/W1/files/')).toBe('/workspace/W1/files/') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx new file mode 100644 index 00000000000..8e76a4244bb --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx @@ -0,0 +1,283 @@ +import { useEffect, useRef, useState } from 'react' +import type { JSONContent } from '@tiptap/core' +import { Image } from '@tiptap/extension-image' +import type { ReactNodeViewProps } from '@tiptap/react' +import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' +import { normalizeLinkHref } from './markdown-fidelity' + +const MIN_WIDTH = 64 + +/** + * A markdown linked image `[![alt](src "t")](href "t2")` — an image wrapped in a link, the canonical + * form of a README badge. `@tiptap/markdown` parses this as a link mark over an image node, but an + * image node can't carry inline marks, so the wrapping link is silently dropped. We instead tokenize + * the whole construct ourselves and hang the link target on the image node's `href` attribute, so it + * round-trips losslessly (and the file stays editable rather than opening read-only). + */ +const LINKED_IMAGE_RE = + /^\[!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/ + +/** Escape a value for safe interpolation into a double-quoted HTML attribute. */ +function escapeAttr(value: string): string { + return value + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>') +} + +/** + * Rewrite an in-app workspace file path (`/workspace/{id}/files/{fileId}`) to its serving endpoint + * (`/api/files/view/{fileId}`) for display only — the stored `src` attribute keeps the original path + * so markdown round-trips unchanged. Absolute and non-workspace URLs pass through untouched. + */ +export function resolveDisplaySrc(src: string | undefined): string | undefined { + if (!src) return src + try { + const parsed = new URL(src, 'http://placeholder') + if (parsed.origin !== 'http://placeholder') return src + const [, seg1, , seg3, fileId] = parsed.pathname.split('/') + if (seg1 === 'workspace' && seg3 === 'files' && fileId) return `/api/files/view/${fileId}` + } catch { + // not a parseable URL — render as-is + } + return src +} + +/** + * Serialize an image to markdown when it has no explicit size, and to an HTML `` tag when + * it does — standard markdown has no width syntax, so a resized image must round-trip as HTML to + * preserve its dimensions. Unsized images stay clean `![alt](src)`. An image with an `href` is + * wrapped in a markdown link so a linked badge round-trips as `[![alt](src)](href)`. + */ +function imageMarkdown(node: JSONContent): string { + const attrs = node.attrs ?? {} + const src = typeof attrs.src === 'string' ? attrs.src : '' + const alt = typeof attrs.alt === 'string' ? attrs.alt : '' + const title = typeof attrs.title === 'string' ? attrs.title : '' + const href = typeof attrs.href === 'string' ? attrs.href : '' + const hrefTitle = typeof attrs.hrefTitle === 'string' ? attrs.hrefTitle : '' + const width = attrs.width + const height = attrs.height + let image: string + if (width || height) { + const parts = [`src="${escapeAttr(src)}"`] + if (alt) parts.push(`alt="${escapeAttr(alt)}"`) + if (title) parts.push(`title="${escapeAttr(title)}"`) + if (width) parts.push(`width="${escapeAttr(String(width))}"`) + if (height) parts.push(`height="${escapeAttr(String(height))}"`) + image = `` + } else { + // Escape so an alt with `]`/`[` or a title with `"` can't break out of the `![…](… "…")` syntax + // and corrupt the round-trip; a src with spaces/parens goes in angle brackets (CommonMark). + const titlePart = title ? ` "${title.replace(/["\\]/g, '\\$&')}"` : '' + const safeSrc = /[\s()]/.test(src) ? `<${src}>` : src + image = `![${alt.replace(/[\\[\]]/g, '\\$&')}](${safeSrc}${titlePart})` + } + if (!href) return image + const hrefTitlePart = hrefTitle ? ` "${hrefTitle}"` : '' + return `[${image}](${href}${hrefTitlePart})` +} + +interface MarkdownImageToken { + /** Set only by our linked-image tokenizer; absent on the built-in `![](src)` token. */ + src?: string + alt?: string + title?: string | null + /** Built-in image token holds the source URL here; our linked token holds the link target. */ + href?: string + hrefTitle?: string | null + /** Built-in image token holds the alt text here. */ + text?: string +} + +/** Map both the built-in image token and our linked-image token onto the image node's attributes. */ +function parseImageToken(token: MarkdownImageToken): JSONContent { + const isLinked = typeof token.src === 'string' + return { + type: 'image', + attrs: isLinked + ? { + src: token.src, + alt: token.alt ?? '', + title: token.title ?? null, + href: token.href ?? null, + hrefTitle: token.hrefTitle ?? null, + } + : { + src: token.href ?? '', + alt: token.text ?? '', + title: token.title ?? null, + href: null, + hrefTitle: null, + }, + } +} + +const widthAttr = { + default: null, + parseHTML: (element: HTMLElement) => element.getAttribute('width'), + renderHTML: (attributes: Record) => + attributes.width ? { width: String(attributes.width) } : {}, +} + +const heightAttr = { + default: null, + parseHTML: (element: HTMLElement) => element.getAttribute('height'), + renderHTML: (attributes: Record) => + attributes.height ? { height: String(attributes.height) } : {}, +} + +/** Link target of a linked image — markdown-only state, never emitted as an HTML `` attribute. */ +const hrefAttr = { default: null, rendered: false } +const hrefTitleAttr = { default: null, rendered: false } + +/** + * Image node that carries optional `width`/`height` (serialized as an HTML `` tag) and an + * optional `href`/`hrefTitle` (a wrapping markdown link, for badges). Shared by the headless + * round-trip path (no node view) and the live {@link ResizableImage}. + */ +export const MarkdownImage = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + width: widthAttr, + height: heightAttr, + href: hrefAttr, + hrefTitle: hrefTitleAttr, + } + }, + markdownTokenizer: { + name: 'image', + level: 'inline', + start: (src: string) => src.indexOf('[!['), + tokenize: (src: string): (MarkdownImageToken & { type: string; raw: string }) | undefined => { + const match = LINKED_IMAGE_RE.exec(src) + if (!match) return undefined + return { + type: 'image', + raw: match[0], + alt: match[1] ?? '', + src: match[2], + title: match[3] ?? null, + href: match[4], + hrefTitle: match[5] ?? null, + } + }, + }, + parseMarkdown: parseImageToken, + renderMarkdown: imageMarkdown, +}) + +/** + * Drag-to-resize image node view (handle at the bottom-right, revealed on selection). Dragging + * commits the new pixel width to the `width` attribute, which serializes to ``. + */ +function ResizableImageView({ node, updateAttributes, selected, editor }: ReactNodeViewProps) { + const imageRef = useRef(null) + const dragAbortRef = useRef(null) + const [dragging, setDragging] = useState(false) + const attrs = node.attrs as { + src?: string + alt?: string + title?: string + width?: string | null + href?: string | null + } + + useEffect(() => () => dragAbortRef.current?.abort(), []) + + const startResize = (event: React.PointerEvent) => { + event.preventDefault() + const image = imageRef.current + if (!image) return + const startX = event.clientX + const startWidth = image.offsetWidth + setDragging(true) + dragAbortRef.current?.abort() + const controller = new AbortController() + dragAbortRef.current = controller + const { signal } = controller + + window.addEventListener( + 'pointermove', + (move) => { + const next = Math.max(MIN_WIDTH, Math.round(startWidth + (move.clientX - startX))) + updateAttributes({ width: String(next) }) + }, + { signal } + ) + window.addEventListener( + 'pointerup', + () => { + setDragging(false) + controller.abort() + }, + { signal } + ) + } + + const widthStyle = attrs.width + ? { width: /^\d+$/.test(attrs.width) ? `${attrs.width}px` : attrs.width } + : undefined + + // Sanitize the linked-image target before rendering the anchor — a parsed markdown href is + // untrusted and could be `javascript:`/`data:`; an unsafe value drops the link (image only). + const safeHref = normalizeLinkHref(typeof attrs.href === 'string' ? attrs.href : '') + + // Read-only: no drag-to-reorder and no resize handle — both call updateAttributes / dispatch a move, + // mutating a doc that must not change. The image still renders (and follows its link on click). + const editable = editor.isEditable + + const image = ( + {attrs.alt + ) + + return ( + + {safeHref ? ( + // The editor's handleClick is the sole navigator (gated on editable/modifier, like text links + // via openOnClick:false): prevent the anchor's own navigation so a plain click in edit mode + // places the caret / selects the node instead of opening a tab. + event.preventDefault()} + > + {image} + + ) : ( + image + )} + {editable && (selected || dragging) && ( +