mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
4 Commits
improvemen
...
v0.6.25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28af223a9f | ||
|
|
d0baf5b1df | ||
|
|
a54dcbe949 | ||
|
|
0b9019d9a2 |
@@ -124,6 +124,29 @@ export function ConditionalIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CredentialIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<circle cx='8' cy='15' r='4' stroke='currentColor' strokeWidth='1.75' />
|
||||
<path d='M11.83 13.17L20 5' stroke='currentColor' strokeWidth='1.75' strokeLinecap='round' />
|
||||
<path
|
||||
d='M18 7l2 2'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.75'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M15 10l2 2'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.75'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4630,6 +4653,59 @@ export function SQSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudFormationIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 80 80'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
>
|
||||
<g
|
||||
id='Icon-Architecture/64/Arch_AWS-CloudFormation_64'
|
||||
stroke='none'
|
||||
strokeWidth='1'
|
||||
fill='none'
|
||||
fillRule='evenodd'
|
||||
>
|
||||
<path
|
||||
d='M53,39.9632039 L58,39.9632039 L58,37.9601375 L53,37.9601375 L53,39.9632039 Z M28,51.9816019 L33,51.9816019 L33,49.9785356 L28,49.9785356 L28,51.9816019 Z M18,51.9816019 L25,51.9816019 L25,49.9785356 L18,49.9785356 L18,51.9816019 Z M18,45.9724029 L30,45.9724029 L30,43.9693366 L18,43.9693366 L18,45.9724029 Z M18,33.9540048 L27,33.9540048 L27,31.9509385 L18,31.9509385 L18,33.9540048 Z M18,39.9632039 L51,39.9632039 L51,37.9601375 L18,37.9601375 L18,39.9632039 Z M37,61.9969337 L14,61.9969337 L14,27.9448058 L37,27.9448058 L37,35.9570712 L39,35.9570712 L39,26.9432726 C39,26.3904263 38.552,25.9417395 38,25.9417395 L13,25.9417395 C12.447,25.9417395 12,26.3904263 12,26.9432726 L12,62.9984668 C12,63.5513131 12.447,64 13,64 L38,64 C38.552,64 39,63.5513131 39,62.9984668 L39,42.9678034 L37,42.9678034 L37,61.9969337 Z M68,36.9586044 C68,43.4305117 62.173,45.6819583 59.092,45.9683968 L43,45.9724029 L43,43.9693366 L59,43.9693366 C59.195,43.9463013 66,43.2121775 66,36.9586044 C66,31.2638867 60.863,30.1081175 59.834,29.9338507 C59.321,29.8467173 58.96,29.3820059 59.004,28.8632117 C59.005,28.8441826 59.007,28.826155 59.009,28.8081274 C58.954,25.5902013 56.981,24.584662 56.126,24.3002266 C54.53,23.769414 52.751,24.2771913 51.81,25.5391231 C51.591,25.8355769 51.229,25.9868085 50.861,25.9307226 C50.497,25.8756383 50.192,25.625255 50.068,25.2767214 C49.447,23.5360568 48.546,22.4083304 47.293,21.1534094 C44.159,18.0386412 39.905,17.1783242 35.925,18.8528877 C33.837,19.7332353 32.012,21.7282894 30.922,24.327268 L29.078,23.5500782 C30.37,20.4743699 32.584,18.0887179 35.15,17.007062 C39.905,15.0049972 44.971,16.0255595 48.704,19.7342369 C49.774,20.8068789 50.66,21.851478 51.35,23.2035478 C52.843,22.0978551 54.857,21.7673492 56.757,22.3993166 C59.189,23.2085554 60.727,25.3207889 60.975,28.1290879 C64.381,28.9884034 68,31.7115721 68,36.9586044 L68,36.9586044 Z'
|
||||
id='AWS-CloudFormation_Icon_64_Squid'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudWatchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 80 80'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
>
|
||||
<g
|
||||
id='Icon-Architecture/64/Arch_Amazon-CloudWatch_64'
|
||||
stroke='none'
|
||||
strokeWidth='1'
|
||||
fill='none'
|
||||
fillRule='evenodd'
|
||||
transform='translate(40, 40) scale(1.25) translate(-40, -40)'
|
||||
>
|
||||
<path
|
||||
d='M55.0592315,46.7773707 C55.0592315,42.8680281 51.8575588,39.6876305 47.9220646,39.6876305 C43.9865705,39.6876305 40.785903,42.8680281 40.785903,46.7773707 C40.785903,50.6867133 43.9865705,53.8671108 47.9220646,53.8671108 C51.8575588,53.8671108 55.0592315,50.6867133 55.0592315,46.7773707 M57.0697011,46.7773707 C57.0697011,51.7881194 52.9663327,55.8642207 47.9220646,55.8642207 C42.8788018,55.8642207 38.7754334,51.7881194 38.7754334,46.7773707 C38.7754334,41.7666219 42.8788018,37.6905206 47.9220646,37.6905206 C52.9663327,37.6905206 57.0697011,41.7666219 57.0697011,46.7773707 M65.5096522,60.4735504 L58.5011554,54.2026253 C57.9352082,54.9944794 57.2808004,55.7174332 56.5540156,56.3634982 L63.5524601,62.6334248 C64.1495696,63.1686502 65.0784065,63.1187225 65.6182176,62.5255808 C66.155013,61.9324392 66.1067617,61.010773 65.5096522,60.4735504 M47.9220646,57.6616197 C53.9645309,57.6616197 58.8801289,52.7786859 58.8801289,46.7773707 C58.8801289,40.7750569 53.9645309,35.8931217 47.9220646,35.8931217 C41.8806036,35.8931217 36.9650056,40.7750569 36.9650056,46.7773707 C36.9650056,52.7786859 41.8806036,57.6616197 47.9220646,57.6616197 M67.1119965,63.8626459 C66.4264264,64.6165549 65.47849,65 64.5285431,65 C63.7002296,65 62.8699057,64.708422 62.207456,64.1172774 L54.9305615,57.5987107 C52.9070239,58.8968321 50.505518,59.6587296 47.9220646,59.6587296 C40.7718297,59.6587296 34.9545361,53.8800921 34.9545361,46.7773707 C34.9545361,39.6746493 40.7718297,33.8960118 47.9220646,33.8960118 C55.0733048,33.8960118 60.8905985,39.6746493 60.8905985,46.7773707 C60.8905985,48.8154213 60.3990387,50.7366411 59.5465996,52.4511599 L66.8556616,58.9906963 C68.2750531,60.265851 68.3896499,62.4496907 67.1119965,63.8626459 M21.2803274,29.392529 C21.2803274,29.9117776 21.3124949,30.429029 21.3738143,30.9293051 C21.4089975,31.2138932 21.3205368,31.4984814 21.1295422,31.7131707 C20.9777518,31.8839236 20.7736891,31.9967603 20.550527,32.0347054 C18.0786547,32.6687878 14.0104695,34.5880104 14.0104695,40.3456782 C14.0104695,44.6933865 16.4240382,47.0929141 18.4495863,48.3411077 C19.1411878,48.7744806 19.9594489,49.0051468 20.8229456,49.0141338 L32.9450717,49.0251179 L32.9430613,51.0222278 L20.811888,51.0112437 C19.5664021,50.9982625 18.384246,50.6607509 17.3840374,50.0346569 C15.3765836,48.7974474 12,45.8896553 12,40.3456782 C12,33.66235 16.5999543,31.191925 19.3000149,30.319188 C19.2799102,30.0116331 19.2698579,29.702081 19.2698579,29.392529 C19.2698579,23.9324305 22.9982737,18.2696254 27.9420183,16.2215892 C33.7241287,13.8150717 39.8500294,15.0083449 44.3263399,19.4109737 C45.7135638,20.7749998 46.8545053,22.4316024 47.7300648,24.3478294 C48.9061895,23.3802296 50.355738,22.8460027 51.8836949,22.8460027 C54.8863312,22.8460027 58.2659305,25.1097268 58.8680661,30.0605622 C61.6797078,30.7046302 67.6206453,32.9553731 67.6206453,40.422567 C67.6206453,43.4042521 66.6797455,45.8666886 64.8230769,47.7419748 L63.3896121,46.3410022 C64.8632863,44.8531553 65.6101757,42.8620367 65.6101757,40.422567 C65.6101757,33.891019 60.1055101,32.2663701 57.737177,31.8719409 C57.4677741,31.827006 57.2295334,31.6752256 57.0757325,31.4515493 C56.9259525,31.2358614 56.8686541,30.9712444 56.9138897,30.7146157 C56.5851779,26.6604826 54.1605516,24.8431126 51.8836949,24.8431126 C50.4472144,24.8431126 49.1001998,25.5381069 48.1874466,26.7503526 C47.9652897,27.0439277 47.6044105,27.193711 47.2344841,27.139789 C46.8695838,27.085867 46.5629872,26.8362283 46.4373329,26.4917268 C45.6140456,24.2260057 44.4278686,22.3207628 42.9119745,20.8309188 C39.0327735,17.0154404 33.7281496,15.9809374 28.7170543,18.0649216 C24.5463352,19.7924217 21.2803274,24.7672224 21.2803274,29.392529'
|
||||
id='Amazon-CloudWatch_Icon_64_Squid'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextractIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
CloudFormationIcon,
|
||||
CloudflareIcon,
|
||||
CloudWatchIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatabricksIcon,
|
||||
@@ -211,6 +213,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
cloudflare: CloudflareIcon,
|
||||
cloudformation: CloudFormationIcon,
|
||||
cloudwatch: CloudWatchIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
databricks: DatabricksIcon,
|
||||
|
||||
183
apps/docs/content/docs/en/tools/cloudformation.mdx
Normal file
183
apps/docs/content/docs/en/tools/cloudformation.mdx
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: CloudFormation
|
||||
description: Manage and inspect AWS CloudFormation stacks, resources, and drift
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="cloudformation"
|
||||
color="linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AWS CloudFormation](https://aws.amazon.com/cloudformation/) is an infrastructure-as-code service that lets you model, provision, and manage AWS resources by treating infrastructure as code. CloudFormation uses templates to describe the resources you need and their dependencies, so you can launch and configure them together as a stack.
|
||||
|
||||
With the CloudFormation integration, you can:
|
||||
|
||||
- **Describe Stacks**: List all stacks in a region or get detailed information about a specific stack, including its status, outputs, tags, and drift information
|
||||
- **List Stack Resources**: Enumerate every resource in a stack with its logical ID, physical ID, type, status, and drift status
|
||||
- **Describe Stack Events**: View the full event history for a stack to understand what happened during create, update, or delete operations
|
||||
- **Detect Stack Drift**: Initiate drift detection to check whether any resources in a stack have been modified outside of CloudFormation
|
||||
- **Drift Detection Status**: Poll the results of a drift detection operation to see which resources have drifted and how many
|
||||
- **Get Template**: Retrieve the original template body (JSON or YAML) used to create or update a stack
|
||||
- **Validate Template**: Check a CloudFormation template for syntax errors, required capabilities, parameters, and declared transforms before deploying
|
||||
|
||||
In Sim, the CloudFormation integration enables your agents to monitor infrastructure state, detect configuration drift, audit stack resources, and validate templates as part of automated SRE and DevOps workflows. This is especially powerful when combined with CloudWatch for observability and SNS for alerting, creating end-to-end infrastructure monitoring pipelines.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `cloudformation_describe_stacks`
|
||||
|
||||
List and describe CloudFormation stacks
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | No | Stack name or ID to describe \(omit to list all stacks\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stacks` | array | List of CloudFormation stacks with status, outputs, and tags |
|
||||
|
||||
### `cloudformation_list_stack_resources`
|
||||
|
||||
List all resources in a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resources` | array | List of stack resources with type, status, and drift information |
|
||||
|
||||
### `cloudformation_detect_stack_drift`
|
||||
|
||||
Initiate drift detection on a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID to detect drift on |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stackDriftDetectionId` | string | ID to use with Describe Stack Drift Detection Status to check results |
|
||||
|
||||
### `cloudformation_describe_stack_drift_detection_status`
|
||||
|
||||
Check the status of a stack drift detection operation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackDriftDetectionId` | string | Yes | The drift detection ID returned by Detect Stack Drift |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stackId` | string | The stack ID |
|
||||
| `stackDriftDetectionId` | string | The drift detection ID |
|
||||
| `stackDriftStatus` | string | Drift status \(DRIFTED, IN_SYNC, NOT_CHECKED\) |
|
||||
| `detectionStatus` | string | Detection status \(DETECTION_IN_PROGRESS, DETECTION_COMPLETE, DETECTION_FAILED\) |
|
||||
| `detectionStatusReason` | string | Reason if detection failed |
|
||||
| `driftedStackResourceCount` | number | Number of resources that have drifted |
|
||||
| `timestamp` | number | Timestamp of the detection |
|
||||
|
||||
### `cloudformation_describe_stack_events`
|
||||
|
||||
Get the event history for a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID |
|
||||
| `limit` | number | No | Maximum number of events to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | List of stack events with resource status and timestamps |
|
||||
|
||||
### `cloudformation_get_template`
|
||||
|
||||
Retrieve the template body for a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `templateBody` | string | The template body as a JSON or YAML string |
|
||||
| `stagesAvailable` | array | Available template stages |
|
||||
|
||||
### `cloudformation_validate_template`
|
||||
|
||||
Validate a CloudFormation template for syntax and structural correctness
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `templateBody` | string | Yes | The CloudFormation template body \(JSON or YAML\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `description` | string | Template description |
|
||||
| `parameters` | array | Template parameters with defaults and descriptions |
|
||||
| `capabilities` | array | Required capabilities \(e.g., CAPABILITY_IAM\) |
|
||||
| `capabilitiesReason` | string | Reason capabilities are required |
|
||||
| `declaredTransforms` | array | Transforms used in the template \(e.g., AWS::Serverless-2016-10-31\) |
|
||||
|
||||
|
||||
180
apps/docs/content/docs/en/tools/cloudwatch.mdx
Normal file
180
apps/docs/content/docs/en/tools/cloudwatch.mdx
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: CloudWatch
|
||||
description: Query and monitor AWS CloudWatch logs, metrics, and alarms
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="cloudwatch"
|
||||
color="linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `cloudwatch_query_logs`
|
||||
|
||||
Run a CloudWatch Log Insights query against one or more log groups
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `logGroupNames` | array | Yes | Log group names to query |
|
||||
| `queryString` | string | Yes | CloudWatch Log Insights query string |
|
||||
| `startTime` | number | Yes | Start time as Unix epoch seconds |
|
||||
| `endTime` | number | Yes | End time as Unix epoch seconds |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Query result rows |
|
||||
| `statistics` | object | Query statistics \(bytesScanned, recordsMatched, recordsScanned\) |
|
||||
| `status` | string | Query completion status |
|
||||
|
||||
### `cloudwatch_describe_log_groups`
|
||||
|
||||
List available CloudWatch log groups
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `prefix` | string | No | Filter log groups by name prefix |
|
||||
| `limit` | number | No | Maximum number of log groups to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `logGroups` | array | List of CloudWatch log groups with metadata |
|
||||
|
||||
### `cloudwatch_get_log_events`
|
||||
|
||||
Retrieve log events from a specific CloudWatch log stream
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `logGroupName` | string | Yes | CloudWatch log group name |
|
||||
| `logStreamName` | string | Yes | CloudWatch log stream name |
|
||||
| `startTime` | number | No | Start time as Unix epoch seconds |
|
||||
| `endTime` | number | No | End time as Unix epoch seconds |
|
||||
| `limit` | number | No | Maximum number of events to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | Log events with timestamp, message, and ingestion time |
|
||||
|
||||
### `cloudwatch_describe_log_streams`
|
||||
|
||||
List log streams within a CloudWatch log group
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `logGroupName` | string | Yes | CloudWatch log group name |
|
||||
| `prefix` | string | No | Filter log streams by name prefix |
|
||||
| `limit` | number | No | Maximum number of log streams to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `logStreams` | array | List of log streams with metadata |
|
||||
|
||||
### `cloudwatch_list_metrics`
|
||||
|
||||
List available CloudWatch metrics
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `namespace` | string | No | Filter by namespace \(e.g., AWS/EC2, AWS/Lambda\) |
|
||||
| `metricName` | string | No | Filter by metric name |
|
||||
| `recentlyActive` | boolean | No | Only show metrics active in the last 3 hours |
|
||||
| `limit` | number | No | Maximum number of metrics to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `metrics` | array | List of metrics with namespace, name, and dimensions |
|
||||
|
||||
### `cloudwatch_get_metric_statistics`
|
||||
|
||||
Get statistics for a CloudWatch metric over a time range
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `namespace` | string | Yes | Metric namespace \(e.g., AWS/EC2, AWS/Lambda\) |
|
||||
| `metricName` | string | Yes | Metric name \(e.g., CPUUtilization, Invocations\) |
|
||||
| `startTime` | number | Yes | Start time as Unix epoch seconds |
|
||||
| `endTime` | number | Yes | End time as Unix epoch seconds |
|
||||
| `period` | number | Yes | Granularity in seconds \(e.g., 60, 300, 3600\) |
|
||||
| `statistics` | array | Yes | Statistics to retrieve \(Average, Sum, Minimum, Maximum, SampleCount\) |
|
||||
| `dimensions` | string | No | Dimensions as JSON \(e.g., \{"InstanceId": "i-1234"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `label` | string | Metric label |
|
||||
| `datapoints` | array | Datapoints with timestamp and statistics values |
|
||||
|
||||
### `cloudwatch_describe_alarms`
|
||||
|
||||
List and filter CloudWatch alarms
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `alarmNamePrefix` | string | No | Filter alarms by name prefix |
|
||||
| `stateValue` | string | No | Filter by alarm state \(OK, ALARM, INSUFFICIENT_DATA\) |
|
||||
| `alarmType` | string | No | Filter by alarm type \(MetricAlarm, CompositeAlarm\) |
|
||||
| `limit` | number | No | Maximum number of alarms to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alarms` | array | List of CloudWatch alarms with state and configuration |
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"clay",
|
||||
"clerk",
|
||||
"cloudflare",
|
||||
"cloudformation",
|
||||
"cloudwatch",
|
||||
"confluence",
|
||||
"cursor",
|
||||
"databricks",
|
||||
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
CloudFormationIcon,
|
||||
CloudflareIcon,
|
||||
CloudWatchIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatabricksIcon,
|
||||
@@ -211,6 +213,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
cloudflare: CloudflareIcon,
|
||||
cloudformation: CloudFormationIcon,
|
||||
cloudwatch: CloudWatchIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
databricks: DatabricksIcon,
|
||||
|
||||
@@ -1912,6 +1912,100 @@
|
||||
"integrationType": "developer-tools",
|
||||
"tags": ["cloud", "monitoring"]
|
||||
},
|
||||
{
|
||||
"type": "cloudformation",
|
||||
"slug": "cloudformation",
|
||||
"name": "CloudFormation",
|
||||
"description": "Manage and inspect AWS CloudFormation stacks, resources, and drift",
|
||||
"longDescription": "Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key.",
|
||||
"bgColor": "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)",
|
||||
"iconName": "CloudFormationIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/cloudformation",
|
||||
"operations": [
|
||||
{
|
||||
"name": "Describe Stacks",
|
||||
"description": "List and describe CloudFormation stacks"
|
||||
},
|
||||
{
|
||||
"name": "List Stack Resources",
|
||||
"description": "List all resources in a CloudFormation stack"
|
||||
},
|
||||
{
|
||||
"name": "Describe Stack Events",
|
||||
"description": "Get the event history for a CloudFormation stack"
|
||||
},
|
||||
{
|
||||
"name": "Detect Stack Drift",
|
||||
"description": "Initiate drift detection on a CloudFormation stack"
|
||||
},
|
||||
{
|
||||
"name": "Drift Detection Status",
|
||||
"description": "Check the status of a stack drift detection operation"
|
||||
},
|
||||
{
|
||||
"name": "Get Template",
|
||||
"description": "Retrieve the template body for a CloudFormation stack"
|
||||
},
|
||||
{
|
||||
"name": "Validate Template",
|
||||
"description": "Validate a CloudFormation template for syntax and structural correctness"
|
||||
}
|
||||
],
|
||||
"operationCount": 7,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "none",
|
||||
"category": "tools",
|
||||
"integrationType": "developer-tools",
|
||||
"tags": ["cloud"]
|
||||
},
|
||||
{
|
||||
"type": "cloudwatch",
|
||||
"slug": "cloudwatch",
|
||||
"name": "CloudWatch",
|
||||
"description": "Query and monitor AWS CloudWatch logs, metrics, and alarms",
|
||||
"longDescription": "Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.",
|
||||
"bgColor": "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)",
|
||||
"iconName": "CloudWatchIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/cloudwatch",
|
||||
"operations": [
|
||||
{
|
||||
"name": "Query Logs (Insights)",
|
||||
"description": "Run a CloudWatch Log Insights query against one or more log groups"
|
||||
},
|
||||
{
|
||||
"name": "Describe Log Groups",
|
||||
"description": "List available CloudWatch log groups"
|
||||
},
|
||||
{
|
||||
"name": "Get Log Events",
|
||||
"description": "Retrieve log events from a specific CloudWatch log stream"
|
||||
},
|
||||
{
|
||||
"name": "Describe Log Streams",
|
||||
"description": "List log streams within a CloudWatch log group"
|
||||
},
|
||||
{
|
||||
"name": "List Metrics",
|
||||
"description": "List available CloudWatch metrics"
|
||||
},
|
||||
{
|
||||
"name": "Get Metric Statistics",
|
||||
"description": "Get statistics for a CloudWatch metric over a time range"
|
||||
},
|
||||
{
|
||||
"name": "Describe Alarms",
|
||||
"description": "List and filter CloudWatch alarms"
|
||||
}
|
||||
],
|
||||
"operationCount": 7,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "none",
|
||||
"category": "tools",
|
||||
"integrationType": "analytics",
|
||||
"tags": ["cloud", "monitoring"]
|
||||
},
|
||||
{
|
||||
"type": "confluence_v2",
|
||||
"slug": "confluence",
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { createFeatureFlagsMock, createMockRequest } from '@sim/testing'
|
||||
import { drizzleOrmMock } from '@sim/testing/mocks'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -13,6 +10,7 @@ const {
|
||||
mockVerifyCronAuth,
|
||||
mockExecuteScheduleJob,
|
||||
mockExecuteJobInline,
|
||||
mockFeatureFlags,
|
||||
mockDbReturning,
|
||||
mockDbUpdate,
|
||||
mockEnqueue,
|
||||
@@ -35,6 +33,12 @@ const {
|
||||
mockVerifyCronAuth: vi.fn().mockReturnValue(null),
|
||||
mockExecuteScheduleJob: vi.fn().mockResolvedValue(undefined),
|
||||
mockExecuteJobInline: vi.fn().mockResolvedValue(undefined),
|
||||
mockFeatureFlags: {
|
||||
isTriggerDevEnabled: false,
|
||||
isHosted: false,
|
||||
isProd: false,
|
||||
isDev: true,
|
||||
},
|
||||
mockDbReturning,
|
||||
mockDbUpdate,
|
||||
mockEnqueue,
|
||||
@@ -45,13 +49,6 @@ const {
|
||||
}
|
||||
})
|
||||
|
||||
const mockFeatureFlags = createFeatureFlagsMock({
|
||||
isTriggerDevEnabled: false,
|
||||
isHosted: false,
|
||||
isProd: false,
|
||||
isDev: true,
|
||||
})
|
||||
|
||||
vi.mock('@/lib/auth/internal', () => ({
|
||||
verifyCronAuth: mockVerifyCronAuth,
|
||||
}))
|
||||
@@ -94,7 +91,17 @@ vi.mock('@/lib/workflows/utils', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => drizzleOrmMock)
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })),
|
||||
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
|
||||
ne: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'ne' })),
|
||||
lte: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'lte' })),
|
||||
lt: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'lt' })),
|
||||
not: vi.fn((condition: unknown) => ({ type: 'not', condition })),
|
||||
isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })),
|
||||
or: vi.fn((...conditions: unknown[]) => ({ type: 'or', conditions })),
|
||||
sql: vi.fn((strings: unknown, ...values: unknown[]) => ({ type: 'sql', strings, values })),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: {
|
||||
@@ -170,13 +177,18 @@ const SINGLE_JOB = [
|
||||
},
|
||||
]
|
||||
|
||||
function createCronRequest() {
|
||||
return createMockRequest(
|
||||
'GET',
|
||||
undefined,
|
||||
{ Authorization: 'Bearer test-cron-secret' },
|
||||
'http://localhost:3000/api/schedules/execute'
|
||||
)
|
||||
function createMockRequest(): NextRequest {
|
||||
const mockHeaders = new Map([
|
||||
['authorization', 'Bearer test-cron-secret'],
|
||||
['content-type', 'application/json'],
|
||||
])
|
||||
|
||||
return {
|
||||
headers: {
|
||||
get: (key: string) => mockHeaders.get(key.toLowerCase()) || null,
|
||||
},
|
||||
url: 'http://localhost:3000/api/schedules/execute',
|
||||
} as NextRequest
|
||||
}
|
||||
|
||||
describe('Scheduled Workflow Execution API Route', () => {
|
||||
@@ -192,7 +204,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
it('should execute scheduled workflows with Trigger.dev disabled', async () => {
|
||||
mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([])
|
||||
|
||||
const response = await GET(createCronRequest() as unknown as NextRequest)
|
||||
const response = await GET(createMockRequest())
|
||||
|
||||
expect(response).toBeDefined()
|
||||
expect(response.status).toBe(200)
|
||||
@@ -205,7 +217,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
mockFeatureFlags.isTriggerDevEnabled = true
|
||||
mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([])
|
||||
|
||||
const response = await GET(createCronRequest() as unknown as NextRequest)
|
||||
const response = await GET(createMockRequest())
|
||||
|
||||
expect(response).toBeDefined()
|
||||
expect(response.status).toBe(200)
|
||||
@@ -216,7 +228,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
it('should handle case with no due schedules', async () => {
|
||||
mockDbReturning.mockReturnValueOnce([]).mockReturnValueOnce([])
|
||||
|
||||
const response = await GET(createCronRequest() as unknown as NextRequest)
|
||||
const response = await GET(createMockRequest())
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
@@ -227,7 +239,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
it('should execute multiple schedules in parallel', async () => {
|
||||
mockDbReturning.mockReturnValueOnce(MULTIPLE_SCHEDULES).mockReturnValueOnce([])
|
||||
|
||||
const response = await GET(createCronRequest() as unknown as NextRequest)
|
||||
const response = await GET(createMockRequest())
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
@@ -237,7 +249,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
it('should queue mothership jobs to BullMQ when available', async () => {
|
||||
mockDbReturning.mockReturnValueOnce([]).mockReturnValueOnce(SINGLE_JOB)
|
||||
|
||||
const response = await GET(createCronRequest() as unknown as NextRequest)
|
||||
const response = await GET(createMockRequest())
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockEnqueueWorkspaceDispatch).toHaveBeenCalledWith(
|
||||
@@ -262,7 +274,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
it('should enqueue preassigned correlation metadata for schedules', async () => {
|
||||
mockDbReturning.mockReturnValue(SINGLE_SCHEDULE)
|
||||
|
||||
const response = await GET(createCronRequest() as unknown as NextRequest)
|
||||
const response = await GET(createMockRequest())
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockEnqueueWorkspaceDispatch).toHaveBeenCalledWith(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
DescribeStackDriftDetectionStatusCommand,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeStackDriftDetectionStatus')
|
||||
|
||||
const DescribeStackDriftDetectionStatusSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
stackDriftDetectionId: z.string().min(1, 'Stack drift detection ID is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeStackDriftDetectionStatusSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new DescribeStackDriftDetectionStatusCommand({
|
||||
StackDriftDetectionId: validatedData.stackDriftDetectionId,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
stackId: response.StackId ?? '',
|
||||
stackDriftDetectionId: response.StackDriftDetectionId ?? '',
|
||||
stackDriftStatus: response.StackDriftStatus,
|
||||
detectionStatus: response.DetectionStatus ?? 'UNKNOWN',
|
||||
detectionStatusReason: response.DetectionStatusReason,
|
||||
driftedStackResourceCount: response.DriftedStackResourceCount,
|
||||
timestamp: response.Timestamp?.getTime(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe stack drift detection status'
|
||||
logger.error('DescribeStackDriftDetectionStatus failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
DescribeStackEventsCommand,
|
||||
type StackEvent,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeStackEvents')
|
||||
|
||||
const DescribeStackEventsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
stackName: z.string().min(1, 'Stack name is required'),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeStackEventsSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const limit = validatedData.limit ?? 50
|
||||
|
||||
const allEvents: StackEvent[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const command = new DescribeStackEventsCommand({
|
||||
StackName: validatedData.stackName,
|
||||
...(nextToken && { NextToken: nextToken }),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
allEvents.push(...(response.StackEvents ?? []))
|
||||
nextToken = allEvents.length >= limit ? undefined : response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
const events = allEvents.slice(0, limit).map((e) => ({
|
||||
stackId: e.StackId ?? '',
|
||||
eventId: e.EventId ?? '',
|
||||
stackName: e.StackName ?? '',
|
||||
logicalResourceId: e.LogicalResourceId,
|
||||
physicalResourceId: e.PhysicalResourceId,
|
||||
resourceType: e.ResourceType,
|
||||
resourceStatus: e.ResourceStatus,
|
||||
resourceStatusReason: e.ResourceStatusReason,
|
||||
timestamp: e.Timestamp?.getTime(),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { events },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudFormation stack events'
|
||||
logger.error('DescribeStackEvents failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
DescribeStacksCommand,
|
||||
type Stack,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeStacks')
|
||||
|
||||
const DescribeStacksSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
stackName: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeStacksSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const allStacks: Stack[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const command = new DescribeStacksCommand({
|
||||
...(validatedData.stackName && { StackName: validatedData.stackName }),
|
||||
...(nextToken && { NextToken: nextToken }),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
allStacks.push(...(response.Stacks ?? []))
|
||||
nextToken = response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
const stacks = allStacks.map((s) => ({
|
||||
stackName: s.StackName ?? '',
|
||||
stackId: s.StackId ?? '',
|
||||
stackStatus: s.StackStatus ?? 'UNKNOWN',
|
||||
stackStatusReason: s.StackStatusReason,
|
||||
creationTime: s.CreationTime?.getTime(),
|
||||
lastUpdatedTime: s.LastUpdatedTime?.getTime(),
|
||||
description: s.Description,
|
||||
enableTerminationProtection: s.EnableTerminationProtection,
|
||||
driftInformation: s.DriftInformation
|
||||
? {
|
||||
stackDriftStatus: s.DriftInformation.StackDriftStatus,
|
||||
lastCheckTimestamp: s.DriftInformation.LastCheckTimestamp?.getTime(),
|
||||
}
|
||||
: null,
|
||||
outputs: (s.Outputs ?? []).map((o) => ({
|
||||
outputKey: o.OutputKey ?? '',
|
||||
outputValue: o.OutputValue ?? '',
|
||||
description: o.Description,
|
||||
})),
|
||||
tags: (s.Tags ?? []).map((t) => ({
|
||||
key: t.Key ?? '',
|
||||
value: t.Value ?? '',
|
||||
})),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { stacks },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudFormation stacks'
|
||||
logger.error('DescribeStacks failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CloudFormationClient, DetectStackDriftCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationDetectStackDrift')
|
||||
|
||||
const DetectStackDriftSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
stackName: z.string().min(1, 'Stack name is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DetectStackDriftSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new DetectStackDriftCommand({
|
||||
StackName: validatedData.stackName,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
if (!response.StackDriftDetectionId) {
|
||||
throw new Error('No drift detection ID returned')
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
stackDriftDetectionId: response.StackDriftDetectionId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to detect CloudFormation stack drift'
|
||||
logger.error('DetectStackDrift failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
53
apps/sim/app/api/tools/cloudformation/get-template/route.ts
Normal file
53
apps/sim/app/api/tools/cloudformation/get-template/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { CloudFormationClient, GetTemplateCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationGetTemplate')
|
||||
|
||||
const GetTemplateSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
stackName: z.string().min(1, 'Stack name is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = GetTemplateSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new GetTemplateCommand({
|
||||
StackName: validatedData.stackName,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
templateBody: response.TemplateBody ?? '',
|
||||
stagesAvailable: response.StagesAvailable ?? [],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get CloudFormation template'
|
||||
logger.error('GetTemplate failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
ListStackResourcesCommand,
|
||||
type StackResourceSummary,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationListStackResources')
|
||||
|
||||
const ListStackResourcesSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
stackName: z.string().min(1, 'Stack name is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = ListStackResourcesSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const allSummaries: StackResourceSummary[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const command = new ListStackResourcesCommand({
|
||||
StackName: validatedData.stackName,
|
||||
...(nextToken && { NextToken: nextToken }),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
allSummaries.push(...(response.StackResourceSummaries ?? []))
|
||||
nextToken = response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
const resources = allSummaries.map((r) => ({
|
||||
logicalResourceId: r.LogicalResourceId ?? '',
|
||||
physicalResourceId: r.PhysicalResourceId,
|
||||
resourceType: r.ResourceType ?? '',
|
||||
resourceStatus: r.ResourceStatus ?? 'UNKNOWN',
|
||||
resourceStatusReason: r.ResourceStatusReason,
|
||||
lastUpdatedTimestamp: r.LastUpdatedTimestamp?.getTime(),
|
||||
driftInformation: r.DriftInformation
|
||||
? {
|
||||
stackResourceDriftStatus: r.DriftInformation.StackResourceDriftStatus,
|
||||
lastCheckTimestamp: r.DriftInformation.LastCheckTimestamp?.getTime(),
|
||||
}
|
||||
: null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { resources },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to list CloudFormation stack resources'
|
||||
logger.error('ListStackResources failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { CloudFormationClient, ValidateTemplateCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudFormationValidateTemplate')
|
||||
|
||||
const ValidateTemplateSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
templateBody: z.string().min(1, 'Template body is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = ValidateTemplateSchema.parse(body)
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new ValidateTemplateCommand({
|
||||
TemplateBody: validatedData.templateBody,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
description: response.Description,
|
||||
parameters: (response.Parameters ?? []).map((p) => ({
|
||||
parameterKey: p.ParameterKey,
|
||||
defaultValue: p.DefaultValue,
|
||||
noEcho: p.NoEcho,
|
||||
description: p.Description,
|
||||
})),
|
||||
capabilities: response.Capabilities ?? [],
|
||||
capabilitiesReason: response.CapabilitiesReason,
|
||||
declaredTransforms: response.DeclaredTransforms ?? [],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to validate CloudFormation template'
|
||||
logger.error('ValidateTemplate failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
|
||||
const datapoints = (response.Datapoints ?? [])
|
||||
.sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0))
|
||||
.map((dp) => ({
|
||||
timestamp: dp.Timestamp ? Math.floor(dp.Timestamp.getTime() / 1000) : 0,
|
||||
timestamp: dp.Timestamp ? dp.Timestamp.getTime() : 0,
|
||||
average: dp.Average,
|
||||
sum: dp.Sum,
|
||||
minimum: dp.Minimum,
|
||||
|
||||
@@ -37,15 +37,18 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
const limit = validatedData.limit ?? 500
|
||||
|
||||
const command = new ListMetricsCommand({
|
||||
...(validatedData.namespace && { Namespace: validatedData.namespace }),
|
||||
...(validatedData.metricName && { MetricName: validatedData.metricName }),
|
||||
...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }),
|
||||
...(limit <= 500 && { MaxResults: limit }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const metrics = (response.Metrics ?? []).slice(0, validatedData.limit ?? 500).map((m) => ({
|
||||
const metrics = (response.Metrics ?? []).slice(0, limit).map((m) => ({
|
||||
namespace: m.Namespace ?? '',
|
||||
metricName: m.MetricName ?? '',
|
||||
dimensions: (m.Dimensions ?? []).map((d) => ({
|
||||
|
||||
@@ -16,8 +16,7 @@ import { workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { exportFolderToZip } from '@/lib/workflows/operations/import-export'
|
||||
import { exportFolderToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import JSZip from 'jszip'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { sanitizePathSegment } from '@/lib/workflows/operations/import-export'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
|
||||
@@ -16,8 +16,7 @@ import { workflow, workflowFolder, workspace } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { exportWorkspaceToZip } from '@/lib/workflows/operations/import-export'
|
||||
import { exportWorkspaceToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { ArrowDown, ArrowUp, Duplicate, Pencil, Trash } from '@/components/emcn/icons'
|
||||
import type { ContextMenuState } from '@/app/workspace/[workspaceId]/tables/[tableId]/types'
|
||||
import type { ContextMenuState } from '../../types'
|
||||
|
||||
interface ContextMenuProps {
|
||||
contextMenu: ContextMenuState
|
||||
|
||||
@@ -17,17 +17,13 @@ import {
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
import type { ColumnDefinition, TableInfo, TableRow } from '@/lib/table'
|
||||
import {
|
||||
cleanCellValue,
|
||||
formatValueForInput,
|
||||
} from '@/app/workspace/[workspaceId]/tables/[tableId]/utils'
|
||||
import {
|
||||
useCreateTableRow,
|
||||
useDeleteTableRow,
|
||||
useDeleteTableRows,
|
||||
useUpdateTableRow,
|
||||
} from '@/hooks/queries/tables'
|
||||
import { useTableUndoStore } from '@/stores/table/store'
|
||||
import { cleanCellValue, formatValueForInput } from '../../utils'
|
||||
|
||||
const logger = createLogger('RowModal')
|
||||
|
||||
@@ -43,9 +39,13 @@ export interface RowModalProps {
|
||||
|
||||
function createInitialRowData(columns: ColumnDefinition[]): Record<string, unknown> {
|
||||
const initial: Record<string, unknown> = {}
|
||||
for (const col of columns) {
|
||||
initial[col.name] = col.type === 'boolean' ? false : ''
|
||||
}
|
||||
columns.forEach((col) => {
|
||||
if (col.type === 'boolean') {
|
||||
initial[col.name] = false
|
||||
} else {
|
||||
initial[col.name] = ''
|
||||
}
|
||||
})
|
||||
return initial
|
||||
}
|
||||
|
||||
@@ -54,13 +54,16 @@ function cleanRowData(
|
||||
rowData: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const cleanData: Record<string, unknown> = {}
|
||||
for (const col of columns) {
|
||||
|
||||
columns.forEach((col) => {
|
||||
const value = rowData[col.name]
|
||||
try {
|
||||
cleanData[col.name] = cleanCellValue(rowData[col.name], col)
|
||||
cleanData[col.name] = cleanCellValue(value, col)
|
||||
} catch {
|
||||
throw new Error(`Invalid JSON for field: ${col.name}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cleanData
|
||||
}
|
||||
|
||||
@@ -83,7 +86,8 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
|
||||
const workspaceId = params.workspaceId as string
|
||||
const tableId = table.id
|
||||
|
||||
const columns = table.schema?.columns || []
|
||||
const schema = table?.schema
|
||||
const columns = schema?.columns || []
|
||||
|
||||
const [rowData, setRowData] = useState<Record<string, unknown>>(() =>
|
||||
getInitialRowData(mode, columns, row)
|
||||
@@ -93,7 +97,6 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
|
||||
const updateRowMutation = useUpdateTableRow({ workspaceId, tableId })
|
||||
const deleteRowMutation = useDeleteTableRow({ workspaceId, tableId })
|
||||
const deleteRowsMutation = useDeleteTableRows({ workspaceId, tableId })
|
||||
const pushToUndoStack = useTableUndoStore((s) => s.push)
|
||||
const isSubmitting =
|
||||
createRowMutation.isPending ||
|
||||
updateRowMutation.isPending ||
|
||||
@@ -108,24 +111,9 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
|
||||
const cleanData = cleanRowData(columns, rowData)
|
||||
|
||||
if (mode === 'add') {
|
||||
const response = await createRowMutation.mutateAsync({ data: cleanData })
|
||||
const createdRow = (response as { data?: { row?: { id?: string; position?: number } } })
|
||||
?.data?.row
|
||||
if (createdRow?.id) {
|
||||
pushToUndoStack(tableId, {
|
||||
type: 'create-row',
|
||||
rowId: createdRow.id,
|
||||
position: createdRow.position ?? 0,
|
||||
data: cleanData,
|
||||
})
|
||||
}
|
||||
await createRowMutation.mutateAsync({ data: cleanData })
|
||||
} else if (mode === 'edit' && row) {
|
||||
const oldData = row.data as Record<string, unknown>
|
||||
await updateRowMutation.mutateAsync({ rowId: row.id, data: cleanData })
|
||||
pushToUndoStack(tableId, {
|
||||
type: 'update-cells',
|
||||
cells: [{ rowId: row.id, oldData, newData: cleanData }],
|
||||
})
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
@@ -141,14 +129,8 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
|
||||
const idsToDelete = rowIds ?? (row ? [row.id] : [])
|
||||
|
||||
try {
|
||||
if (idsToDelete.length === 1 && row) {
|
||||
if (idsToDelete.length === 1) {
|
||||
await deleteRowMutation.mutateAsync(idsToDelete[0])
|
||||
pushToUndoStack(tableId, {
|
||||
type: 'delete-rows',
|
||||
rows: [
|
||||
{ rowId: row.id, data: row.data as Record<string, unknown>, position: row.position },
|
||||
],
|
||||
})
|
||||
} else {
|
||||
await deleteRowsMutation.mutateAsync(idsToDelete)
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export type { TableFilterHandle } from './table-filter'
|
||||
export { TableFilter } from './table-filter'
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { nanoid } from 'nanoid'
|
||||
import {
|
||||
@@ -27,42 +19,22 @@ const OPERATOR_LABELS = Object.fromEntries(
|
||||
COMPARISON_OPERATORS.map((op) => [op.value, op.label])
|
||||
) as Record<string, string>
|
||||
|
||||
export interface TableFilterHandle {
|
||||
addColumnRule: (columnName: string) => void
|
||||
}
|
||||
|
||||
interface TableFilterProps {
|
||||
columns: Array<{ name: string; type: string }>
|
||||
filter: Filter | null
|
||||
onApply: (filter: Filter | null) => void
|
||||
onClose: () => void
|
||||
initialColumn?: string | null
|
||||
}
|
||||
|
||||
export const TableFilter = forwardRef<TableFilterHandle, TableFilterProps>(function TableFilter(
|
||||
{ columns, filter, onApply, onClose, initialColumn },
|
||||
ref
|
||||
) {
|
||||
export function TableFilter({ columns, filter, onApply, onClose }: TableFilterProps) {
|
||||
const [rules, setRules] = useState<FilterRule[]>(() => {
|
||||
const fromFilter = filterToRules(filter)
|
||||
if (fromFilter.length > 0) return fromFilter
|
||||
const rule = createRule(columns)
|
||||
return [initialColumn ? { ...rule, column: initialColumn } : rule]
|
||||
return fromFilter.length > 0 ? fromFilter : [createRule(columns)]
|
||||
})
|
||||
|
||||
const rulesRef = useRef(rules)
|
||||
rulesRef.current = rules
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
addColumnRule: (columnName: string) => {
|
||||
setRules((prev) => [...prev, { ...createRule(columns), column: columnName }])
|
||||
},
|
||||
}),
|
||||
[columns]
|
||||
)
|
||||
|
||||
const columnOptions = useMemo(
|
||||
() => columns.map((col) => ({ value: col.name, label: col.name })),
|
||||
[columns]
|
||||
@@ -153,7 +125,7 @@ export const TableFilter = forwardRef<TableFilterHandle, TableFilterProps>(funct
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
interface FilterRuleRowProps {
|
||||
rule: FilterRule
|
||||
|
||||
@@ -24,15 +24,11 @@ import {
|
||||
Skeleton,
|
||||
} from '@/components/emcn'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
Calendar as CalendarIcon,
|
||||
ChevronDown,
|
||||
Download,
|
||||
Fingerprint,
|
||||
ListFilter,
|
||||
Pencil,
|
||||
Plus,
|
||||
Table as TableIcon,
|
||||
@@ -49,26 +45,6 @@ import type { ColumnDefinition, Filter, SortDirection, TableRow as TableRowType
|
||||
import type { ColumnOption, SortConfig } from '@/app/workspace/[workspaceId]/components'
|
||||
import { ResourceHeader, ResourceOptionsBar } from '@/app/workspace/[workspaceId]/components'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { ContextMenu } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/context-menu'
|
||||
import { RowModal } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal'
|
||||
import type { TableFilterHandle } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter'
|
||||
import { TableFilter } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter'
|
||||
import {
|
||||
useContextMenu,
|
||||
useExportTable,
|
||||
useTableData,
|
||||
} from '@/app/workspace/[workspaceId]/tables/[tableId]/hooks'
|
||||
import type {
|
||||
EditingCell,
|
||||
QueryOptions,
|
||||
SaveReason,
|
||||
} from '@/app/workspace/[workspaceId]/tables/[tableId]/types'
|
||||
import {
|
||||
cleanCellValue,
|
||||
displayToStorage,
|
||||
formatValueForInput,
|
||||
storageToDisplay,
|
||||
} from '@/app/workspace/[workspaceId]/tables/[tableId]/utils'
|
||||
import {
|
||||
useAddTableColumn,
|
||||
useBatchCreateTableRows,
|
||||
@@ -84,6 +60,17 @@ import {
|
||||
import { useInlineRename } from '@/hooks/use-inline-rename'
|
||||
import { extractCreatedRowId, useTableUndo } from '@/hooks/use-table-undo'
|
||||
import type { DeletedRowSnapshot } from '@/stores/table/types'
|
||||
import { useContextMenu, useTableData } from '../../hooks'
|
||||
import type { EditingCell, QueryOptions, SaveReason } from '../../types'
|
||||
import {
|
||||
cleanCellValue,
|
||||
displayToStorage,
|
||||
formatValueForInput,
|
||||
storageToDisplay,
|
||||
} from '../../utils'
|
||||
import { ContextMenu } from '../context-menu'
|
||||
import { RowModal } from '../row-modal'
|
||||
import { TableFilter } from '../table-filter'
|
||||
|
||||
interface CellCoord {
|
||||
rowIndex: number
|
||||
@@ -101,7 +88,6 @@ interface NormalizedSelection {
|
||||
|
||||
const EMPTY_COLUMNS: never[] = []
|
||||
const EMPTY_CHECKED_ROWS = new Set<number>()
|
||||
const clearCheckedRows = (prev: Set<number>) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)
|
||||
const COL_WIDTH = 160
|
||||
const COL_WIDTH_MIN = 80
|
||||
const CHECKBOX_COL_WIDTH = 40
|
||||
@@ -210,7 +196,6 @@ export function Table({
|
||||
const [initialCharacter, setInitialCharacter] = useState<string | null>(null)
|
||||
const [selectionAnchor, setSelectionAnchor] = useState<CellCoord | null>(null)
|
||||
const [selectionFocus, setSelectionFocus] = useState<CellCoord | null>(null)
|
||||
const [isColumnSelection, setIsColumnSelection] = useState(false)
|
||||
const [checkedRows, setCheckedRows] = useState(EMPTY_CHECKED_ROWS)
|
||||
const lastCheckboxRowRef = useRef<number | null>(null)
|
||||
const [showDeleteTableConfirm, setShowDeleteTableConfirm] = useState(false)
|
||||
@@ -235,7 +220,6 @@ export function Table({
|
||||
const metadataSeededRef = useRef(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const tableFilterRef = useRef<TableFilterHandle>(null)
|
||||
const isDraggingRef = useRef(false)
|
||||
|
||||
const { tableData, isLoadingTable, rows, isLoadingRows } = useTableData({
|
||||
@@ -307,11 +291,10 @@ export function Table({
|
||||
const positionMapRef = useRef(positionMap)
|
||||
positionMapRef.current = positionMap
|
||||
|
||||
const normalizedSelection = useMemo(() => {
|
||||
const raw = computeNormalizedSelection(selectionAnchor, selectionFocus)
|
||||
if (!raw || !isColumnSelection) return raw
|
||||
return { ...raw, startRow: 0, endRow: Math.max(maxPosition, 0) }
|
||||
}, [selectionAnchor, selectionFocus, isColumnSelection, maxPosition])
|
||||
const normalizedSelection = useMemo(
|
||||
() => computeNormalizedSelection(selectionAnchor, selectionFocus),
|
||||
[selectionAnchor, selectionFocus]
|
||||
)
|
||||
|
||||
const displayColCount = isLoadingTable ? SKELETON_COL_COUNT : displayColumns.length
|
||||
const tableWidth = useMemo(() => {
|
||||
@@ -332,18 +315,7 @@ export function Table({
|
||||
}, [resizingColumn, displayColumns, columnWidths])
|
||||
|
||||
const dropIndicatorLeft = useMemo(() => {
|
||||
if (!dropTargetColumnName || !dragColumnName) return null
|
||||
|
||||
const dragIdx = displayColumns.findIndex((c) => c.name === dragColumnName)
|
||||
const targetIdx = displayColumns.findIndex((c) => c.name === dropTargetColumnName)
|
||||
|
||||
if (dragIdx !== -1 && targetIdx !== -1) {
|
||||
// Suppress when drop would be a no-op (same effective position)
|
||||
if (targetIdx === dragIdx) return null
|
||||
if (dropSide === 'right' && targetIdx === dragIdx - 1) return null
|
||||
if (dropSide === 'left' && targetIdx === dragIdx + 1) return null
|
||||
}
|
||||
|
||||
if (!dropTargetColumnName) return null
|
||||
let left = CHECKBOX_COL_WIDTH
|
||||
for (const col of displayColumns) {
|
||||
if (dropSide === 'left' && col.name === dropTargetColumnName) return left
|
||||
@@ -351,7 +323,7 @@ export function Table({
|
||||
if (dropSide === 'right' && col.name === dropTargetColumnName) return left
|
||||
}
|
||||
return null
|
||||
}, [dropTargetColumnName, dropSide, displayColumns, columnWidths, dragColumnName])
|
||||
}, [dropTargetColumnName, dropSide, displayColumns, columnWidths])
|
||||
|
||||
const isAllRowsSelected = useMemo(() => {
|
||||
if (checkedRows.size > 0 && rows.length > 0 && checkedRows.size >= rows.length) {
|
||||
@@ -378,7 +350,6 @@ export function Table({
|
||||
const rowsRef = useRef(rows)
|
||||
const selectionAnchorRef = useRef(selectionAnchor)
|
||||
const selectionFocusRef = useRef(selectionFocus)
|
||||
const normalizedSelectionRef = useRef(normalizedSelection)
|
||||
|
||||
const checkedRowsRef = useRef(checkedRows)
|
||||
checkedRowsRef.current = checkedRows
|
||||
@@ -388,7 +359,6 @@ export function Table({
|
||||
rowsRef.current = rows
|
||||
selectionAnchorRef.current = selectionAnchor
|
||||
selectionFocusRef.current = selectionFocus
|
||||
normalizedSelectionRef.current = normalizedSelection
|
||||
|
||||
const deleteTableMutation = useDeleteTable(workspaceId)
|
||||
const renameTableMutation = useRenameTable(workspaceId)
|
||||
@@ -604,8 +574,7 @@ export function Table({
|
||||
|
||||
const handleCellMouseDown = useCallback(
|
||||
(rowIndex: number, colIndex: number, shiftKey: boolean) => {
|
||||
setCheckedRows(clearCheckedRows)
|
||||
setIsColumnSelection(false)
|
||||
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
|
||||
lastCheckboxRowRef.current = null
|
||||
if (shiftKey && selectionAnchorRef.current) {
|
||||
setSelectionFocus({ rowIndex, colIndex })
|
||||
@@ -628,7 +597,6 @@ export function Table({
|
||||
setEditingCell(null)
|
||||
setSelectionAnchor(null)
|
||||
setSelectionFocus(null)
|
||||
setIsColumnSelection(false)
|
||||
|
||||
if (shiftKey && lastCheckboxRowRef.current !== null) {
|
||||
const from = Math.min(lastCheckboxRowRef.current, rowIndex)
|
||||
@@ -659,8 +627,7 @@ export function Table({
|
||||
const handleClearSelection = useCallback(() => {
|
||||
setSelectionAnchor(null)
|
||||
setSelectionFocus(null)
|
||||
setIsColumnSelection(false)
|
||||
setCheckedRows(clearCheckedRows)
|
||||
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
|
||||
lastCheckboxRowRef.current = null
|
||||
}, [])
|
||||
|
||||
@@ -670,7 +637,6 @@ export function Table({
|
||||
setEditingCell(null)
|
||||
setSelectionAnchor(null)
|
||||
setSelectionFocus(null)
|
||||
setIsColumnSelection(false)
|
||||
const all = new Set<number>()
|
||||
for (const row of rws) {
|
||||
all.add(row.position)
|
||||
@@ -716,22 +682,21 @@ export function Table({
|
||||
const target = dropTargetColumnNameRef.current
|
||||
const side = dropSideRef.current
|
||||
if (target && dragged !== target) {
|
||||
const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.name)
|
||||
const newOrder = currentOrder.filter((n) => n !== dragged)
|
||||
const targetIndex = newOrder.indexOf(target)
|
||||
if (targetIndex === -1) {
|
||||
setDragColumnName(null)
|
||||
setDropTargetColumnName(null)
|
||||
setDropSide('left')
|
||||
return
|
||||
const cols = columnsRef.current
|
||||
const currentOrder = columnOrderRef.current ?? cols.map((c) => c.name)
|
||||
const fromIndex = currentOrder.indexOf(dragged)
|
||||
const toIndex = currentOrder.indexOf(target)
|
||||
if (fromIndex !== -1 && toIndex !== -1) {
|
||||
const newOrder = currentOrder.filter((n) => n !== dragged)
|
||||
let insertIndex = newOrder.indexOf(target)
|
||||
if (side === 'right') insertIndex += 1
|
||||
newOrder.splice(insertIndex, 0, dragged)
|
||||
setColumnOrder(newOrder)
|
||||
updateMetadataRef.current({
|
||||
columnWidths: columnWidthsRef.current,
|
||||
columnOrder: newOrder,
|
||||
})
|
||||
}
|
||||
const insertIndex = side === 'right' ? targetIndex + 1 : targetIndex
|
||||
newOrder.splice(insertIndex, 0, dragged)
|
||||
setColumnOrder(newOrder)
|
||||
updateMetadataRef.current({
|
||||
columnWidths: columnWidthsRef.current,
|
||||
columnOrder: newOrder,
|
||||
})
|
||||
}
|
||||
setDragColumnName(null)
|
||||
setDropTargetColumnName(null)
|
||||
@@ -817,9 +782,6 @@ export function Table({
|
||||
const updateMetadataRef = useRef(updateMetadataMutation.mutate)
|
||||
updateMetadataRef.current = updateMetadataMutation.mutate
|
||||
|
||||
const addColumnAsyncRef = useRef(addColumnMutation.mutateAsync)
|
||||
addColumnAsyncRef.current = addColumnMutation.mutateAsync
|
||||
|
||||
const toggleBooleanCellRef = useRef(toggleBooleanCell)
|
||||
toggleBooleanCellRef.current = toggleBooleanCell
|
||||
|
||||
@@ -832,21 +794,7 @@ export function Table({
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const tag = (e.target as HTMLElement).tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
||||
if (e.key === 'Escape') setIsColumnSelection(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
isDraggingRef.current = false
|
||||
setSelectionAnchor(null)
|
||||
setSelectionFocus(null)
|
||||
setIsColumnSelection(false)
|
||||
setCheckedRows(clearCheckedRows)
|
||||
lastCheckboxRowRef.current = null
|
||||
return
|
||||
}
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'y')) {
|
||||
e.preventDefault()
|
||||
@@ -858,6 +806,15 @@ export function Table({
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
setSelectionAnchor(null)
|
||||
setSelectionFocus(null)
|
||||
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
|
||||
lastCheckboxRowRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
||||
e.preventDefault()
|
||||
const rws = rowsRef.current
|
||||
@@ -865,7 +822,6 @@ export function Table({
|
||||
setEditingCell(null)
|
||||
setSelectionAnchor(null)
|
||||
setSelectionFocus(null)
|
||||
setIsColumnSelection(false)
|
||||
const all = new Set<number>()
|
||||
for (const row of rws) {
|
||||
all.add(row.position)
|
||||
@@ -879,7 +835,6 @@ export function Table({
|
||||
const a = selectionAnchorRef.current
|
||||
if (!a || editingCellRef.current) return
|
||||
e.preventDefault()
|
||||
setIsColumnSelection(false)
|
||||
setSelectionFocus(null)
|
||||
setCheckedRows((prev) => {
|
||||
const next = new Set(prev)
|
||||
@@ -932,7 +887,6 @@ export function Table({
|
||||
const row = positionMapRef.current.get(anchor.rowIndex)
|
||||
if (!row) return
|
||||
e.preventDefault()
|
||||
setIsColumnSelection(false)
|
||||
const position = row.position + 1
|
||||
const colIndex = anchor.colIndex
|
||||
createRef.current(
|
||||
@@ -954,12 +908,12 @@ export function Table({
|
||||
if (e.key === 'Enter' || e.key === 'F2') {
|
||||
if (!canEditRef.current) return
|
||||
e.preventDefault()
|
||||
setIsColumnSelection(false)
|
||||
const col = cols[anchor.colIndex]
|
||||
if (!col) return
|
||||
|
||||
const row = positionMapRef.current.get(anchor.rowIndex)
|
||||
if (!row) return
|
||||
|
||||
if (col.type === 'boolean') {
|
||||
toggleBooleanCellRef.current(row.id, col.name, row.data[col.name])
|
||||
return
|
||||
@@ -981,8 +935,7 @@ export function Table({
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
setCheckedRows(clearCheckedRows)
|
||||
setIsColumnSelection(false)
|
||||
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
|
||||
lastCheckboxRowRef.current = null
|
||||
setSelectionAnchor(moveCell(anchor, cols.length, totalRows, e.shiftKey ? -1 : 1))
|
||||
setSelectionFocus(null)
|
||||
@@ -991,8 +944,7 @@ export function Table({
|
||||
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
setCheckedRows(clearCheckedRows)
|
||||
setIsColumnSelection(false)
|
||||
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
|
||||
lastCheckboxRowRef.current = null
|
||||
const focus = selectionFocusRef.current ?? anchor
|
||||
const origin = e.shiftKey ? focus : anchor
|
||||
@@ -1027,7 +979,7 @@ export function Table({
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
if (!canEditRef.current) return
|
||||
e.preventDefault()
|
||||
const sel = normalizedSelectionRef.current
|
||||
const sel = computeNormalizedSelection(anchor, selectionFocusRef.current)
|
||||
if (!sel) return
|
||||
const pMap = positionMapRef.current
|
||||
const undoCells: Array<{ rowId: string; data: Record<string, unknown> }> = []
|
||||
@@ -1059,7 +1011,6 @@ export function Table({
|
||||
if (col.type === 'number' && !/[\d.-]/.test(e.key)) return
|
||||
if (col.type === 'date' && !/[\d\-/]/.test(e.key)) return
|
||||
e.preventDefault()
|
||||
setIsColumnSelection(false)
|
||||
|
||||
const row = positionMapRef.current.get(anchor.rowIndex)
|
||||
if (!row) return
|
||||
@@ -1096,7 +1047,10 @@ export function Table({
|
||||
return
|
||||
}
|
||||
|
||||
const sel = normalizedSelectionRef.current
|
||||
const anchor = selectionAnchorRef.current
|
||||
if (!anchor) return
|
||||
|
||||
const sel = computeNormalizedSelection(anchor, selectionFocusRef.current)
|
||||
if (!sel) return
|
||||
|
||||
e.preventDefault()
|
||||
@@ -1152,7 +1106,10 @@ export function Table({
|
||||
}
|
||||
e.clipboardData?.setData('text/plain', lines.join('\n'))
|
||||
} else {
|
||||
const sel = normalizedSelectionRef.current
|
||||
const anchor = selectionAnchorRef.current
|
||||
if (!anchor) return
|
||||
|
||||
const sel = computeNormalizedSelection(anchor, selectionFocusRef.current)
|
||||
if (!sel) return
|
||||
|
||||
e.preventDefault()
|
||||
@@ -1188,7 +1145,7 @@ export function Table({
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = async (e: ClipboardEvent) => {
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
const tag = (e.target as HTMLElement).tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return
|
||||
if (!canEditRef.current) return
|
||||
@@ -1207,48 +1164,8 @@ export function Table({
|
||||
|
||||
if (pasteRows.length === 0) return
|
||||
|
||||
let currentCols = columnsRef.current
|
||||
const currentCols = columnsRef.current
|
||||
const pMap = positionMapRef.current
|
||||
const maxPasteCols = Math.max(...pasteRows.map((pr) => pr.length))
|
||||
const neededExtraCols = Math.max(
|
||||
0,
|
||||
currentAnchor.colIndex + maxPasteCols - currentCols.length
|
||||
)
|
||||
|
||||
if (neededExtraCols > 0) {
|
||||
// Generate unique names for the new columns without colliding with each other
|
||||
const existingNames = new Set(currentCols.map((c) => c.name.toLowerCase()))
|
||||
const newColNames: string[] = []
|
||||
for (let i = 0; i < neededExtraCols; i++) {
|
||||
let name = 'untitled'
|
||||
let n = 2
|
||||
while (existingNames.has(name.toLowerCase())) {
|
||||
name = `untitled_${n}`
|
||||
n++
|
||||
}
|
||||
existingNames.add(name.toLowerCase())
|
||||
newColNames.push(name)
|
||||
}
|
||||
|
||||
// Create columns sequentially so each invalidation completes before the next
|
||||
const createdColNames: string[] = []
|
||||
try {
|
||||
for (const name of newColNames) {
|
||||
await addColumnAsyncRef.current({ name, type: 'string' })
|
||||
createdColNames.push(name)
|
||||
}
|
||||
} catch {
|
||||
// If column creation fails partway, paste into whatever columns were created
|
||||
}
|
||||
|
||||
// Build updated column list locally — React Query cache may not have refreshed yet
|
||||
if (createdColNames.length > 0) {
|
||||
currentCols = [
|
||||
...currentCols,
|
||||
...createdColNames.map((name) => ({ name, type: 'string' as const })),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const undoCells: Array<{ rowId: string; data: Record<string, unknown> }> = []
|
||||
const updateBatch: Array<{ rowId: string; data: Record<string, unknown> }> = []
|
||||
@@ -1328,6 +1245,7 @@ export function Table({
|
||||
)
|
||||
}
|
||||
|
||||
const maxPasteCols = Math.max(...pasteRows.map((pr) => pr.length))
|
||||
setSelectionFocus({
|
||||
rowIndex: currentAnchor.rowIndex + pasteRows.length - 1,
|
||||
colIndex: Math.min(currentAnchor.colIndex + maxPasteCols - 1, currentCols.length - 1),
|
||||
@@ -1403,10 +1321,10 @@ export function Table({
|
||||
}, [])
|
||||
|
||||
const generateColumnName = useCallback(() => {
|
||||
const existing = new Set(schemaColumnsRef.current.map((c) => c.name.toLowerCase()))
|
||||
const existing = schemaColumnsRef.current.map((c) => c.name.toLowerCase())
|
||||
let name = 'untitled'
|
||||
let i = 2
|
||||
while (existing.has(name)) {
|
||||
while (existing.includes(name.toLowerCase())) {
|
||||
name = `untitled_${i}`
|
||||
i++
|
||||
}
|
||||
@@ -1511,10 +1429,7 @@ export function Table({
|
||||
}, [])
|
||||
|
||||
const handleRenameColumn = useCallback(
|
||||
(name: string) => {
|
||||
isDraggingRef.current = false
|
||||
columnRename.startRename(name, name)
|
||||
},
|
||||
(name: string) => columnRename.startRename(name, name),
|
||||
[columnRename.startRename]
|
||||
)
|
||||
|
||||
@@ -1525,22 +1440,10 @@ export function Table({
|
||||
const handleDeleteColumnConfirm = useCallback(() => {
|
||||
if (!deletingColumn) return
|
||||
const columnToDelete = deletingColumn
|
||||
const column = schemaColumnsRef.current.find((c) => c.name === columnToDelete)
|
||||
const position = schemaColumnsRef.current.findIndex((c) => c.name === columnToDelete)
|
||||
const orderAtDelete = columnOrderRef.current
|
||||
setDeletingColumn(null)
|
||||
deleteColumnMutation.mutate(columnToDelete, {
|
||||
onSuccess: () => {
|
||||
if (column && position !== -1) {
|
||||
pushUndoRef.current({
|
||||
type: 'delete-column',
|
||||
columnName: columnToDelete,
|
||||
columnType: column.type,
|
||||
position,
|
||||
unique: !!column.unique,
|
||||
required: !!column.required,
|
||||
})
|
||||
}
|
||||
if (!orderAtDelete) return
|
||||
const newOrder = orderAtDelete.filter((n) => n !== columnToDelete)
|
||||
setColumnOrder(newOrder)
|
||||
@@ -1565,28 +1468,13 @@ export function Table({
|
||||
}, [])
|
||||
|
||||
const [filterOpen, setFilterOpen] = useState(false)
|
||||
const [initialFilterColumn, setInitialFilterColumn] = useState<string | null>(null)
|
||||
|
||||
const handleFilterToggle = useCallback(() => {
|
||||
setInitialFilterColumn(null)
|
||||
setFilterOpen((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const handleFilterClose = useCallback(() => {
|
||||
setFilterOpen(false)
|
||||
setInitialFilterColumn(null)
|
||||
}, [])
|
||||
|
||||
const filterOpenRef = useRef(filterOpen)
|
||||
filterOpenRef.current = filterOpen
|
||||
|
||||
const handleFilterByColumn = useCallback((columnName: string) => {
|
||||
if (filterOpenRef.current && tableFilterRef.current) {
|
||||
tableFilterRef.current.addColumnRule(columnName)
|
||||
} else {
|
||||
setInitialFilterColumn(columnName)
|
||||
setFilterOpen(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const columnOptions = useMemo<ColumnOption[]>(
|
||||
@@ -1667,27 +1555,6 @@ export function Table({
|
||||
[handleAddColumn, addColumnMutation.isPending]
|
||||
)
|
||||
|
||||
const { handleExportTable, isExporting } = useExportTable({
|
||||
workspaceId,
|
||||
tableId,
|
||||
tableName: tableData?.name,
|
||||
columns: displayColumns,
|
||||
queryOptions,
|
||||
canExport: userPermissions.canEdit,
|
||||
})
|
||||
|
||||
const headerActions = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: isExporting ? 'Exporting...' : 'Export CSV',
|
||||
icon: Download,
|
||||
onClick: () => void handleExportTable(),
|
||||
disabled: !userPermissions.canEdit || !hasTableData || isLoadingTable || isExporting,
|
||||
},
|
||||
],
|
||||
[handleExportTable, hasTableData, isExporting, isLoadingTable, userPermissions.canEdit]
|
||||
)
|
||||
|
||||
const activeSortState = useMemo(() => {
|
||||
if (!queryOptions.sort) return null
|
||||
const entries = Object.entries(queryOptions.sort)
|
||||
@@ -1696,32 +1563,6 @@ export function Table({
|
||||
return { column, direction }
|
||||
}, [queryOptions.sort])
|
||||
|
||||
const selectedColumnRange = useMemo(() => {
|
||||
if (!isColumnSelection || !normalizedSelection) return null
|
||||
return { start: normalizedSelection.startCol, end: normalizedSelection.endCol }
|
||||
}, [isColumnSelection, normalizedSelection])
|
||||
|
||||
const draggingColIndex = useMemo(
|
||||
() => (dragColumnName ? displayColumns.findIndex((c) => c.name === dragColumnName) : null),
|
||||
[dragColumnName, displayColumns]
|
||||
)
|
||||
|
||||
const handleColumnSelect = useCallback((colIndex: number) => {
|
||||
setSelectionAnchor({ rowIndex: 0, colIndex })
|
||||
setSelectionFocus({ rowIndex: 0, colIndex })
|
||||
setIsColumnSelection(true)
|
||||
}, [])
|
||||
|
||||
const handleSortAsc = useCallback(
|
||||
(columnName: string) => handleSortChange(columnName, 'asc'),
|
||||
[handleSortChange]
|
||||
)
|
||||
|
||||
const handleSortDesc = useCallback(
|
||||
(columnName: string) => handleSortChange(columnName, 'desc'),
|
||||
[handleSortChange]
|
||||
)
|
||||
|
||||
const sortConfig = useMemo<SortConfig>(
|
||||
() => ({
|
||||
options: columnOptions,
|
||||
@@ -1778,12 +1619,7 @@ export function Table({
|
||||
<div ref={containerRef} className='flex h-full flex-col overflow-hidden'>
|
||||
{!embedded && (
|
||||
<>
|
||||
<ResourceHeader
|
||||
icon={TableIcon}
|
||||
breadcrumbs={breadcrumbs}
|
||||
actions={headerActions}
|
||||
create={createAction}
|
||||
/>
|
||||
<ResourceHeader icon={TableIcon} breadcrumbs={breadcrumbs} create={createAction} />
|
||||
|
||||
<ResourceOptionsBar
|
||||
sort={sortConfig}
|
||||
@@ -1792,12 +1628,10 @@ export function Table({
|
||||
/>
|
||||
{filterOpen && (
|
||||
<TableFilter
|
||||
ref={tableFilterRef}
|
||||
columns={displayColumns}
|
||||
filter={queryOptions.filter}
|
||||
onApply={handleFilterApply}
|
||||
onClose={handleFilterClose}
|
||||
initialColumn={initialFilterColumn}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -1857,11 +1691,10 @@ export function Table({
|
||||
checked={isAllRowsSelected}
|
||||
onCheckedChange={handleSelectAllToggle}
|
||||
/>
|
||||
{displayColumns.map((column, colIndex) => (
|
||||
{displayColumns.map((column) => (
|
||||
<ColumnHeaderMenu
|
||||
key={column.name}
|
||||
column={column}
|
||||
colIndex={colIndex}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
isRenaming={columnRename.editingId === column.name}
|
||||
renameValue={
|
||||
@@ -1880,20 +1713,10 @@ export function Table({
|
||||
onResize={handleColumnResize}
|
||||
onResizeEnd={handleColumnResizeEnd}
|
||||
isDragging={dragColumnName === column.name}
|
||||
isDropTarget={
|
||||
dropTargetColumnName === column.name && dropIndicatorLeft !== null
|
||||
}
|
||||
onDragStart={handleColumnDragStart}
|
||||
onDragOver={handleColumnDragOver}
|
||||
onDragEnd={handleColumnDragEnd}
|
||||
onDragLeave={handleColumnDragLeave}
|
||||
sortDirection={
|
||||
activeSortState?.column === column.name ? activeSortState.direction : null
|
||||
}
|
||||
onSortAsc={handleSortAsc}
|
||||
onSortDesc={handleSortDesc}
|
||||
onFilterColumn={handleFilterByColumn}
|
||||
onColumnSelect={handleColumnSelect}
|
||||
/>
|
||||
))}
|
||||
{userPermissions.canEdit && (
|
||||
@@ -1921,7 +1744,6 @@ export function Table({
|
||||
startPosition={prevPosition + 1}
|
||||
columns={displayColumns}
|
||||
normalizedSelection={normalizedSelection}
|
||||
draggingColIndex={draggingColIndex}
|
||||
checkedRows={checkedRows}
|
||||
firstRowUnderHeader={prevPosition === -1}
|
||||
onCellMouseDown={handleCellMouseDown}
|
||||
@@ -1944,7 +1766,6 @@ export function Table({
|
||||
: null
|
||||
}
|
||||
normalizedSelection={normalizedSelection}
|
||||
draggingColIndex={draggingColIndex}
|
||||
onClick={handleCellClick}
|
||||
onDoubleClick={handleCellDoubleClick}
|
||||
onSave={handleInlineSave}
|
||||
@@ -2096,7 +1917,6 @@ interface PositionGapRowsProps {
|
||||
startPosition: number
|
||||
columns: ColumnDefinition[]
|
||||
normalizedSelection: NormalizedSelection | null
|
||||
draggingColIndex: number | null
|
||||
checkedRows: Set<number>
|
||||
firstRowUnderHeader?: boolean
|
||||
onCellMouseDown: (rowIndex: number, colIndex: number, shiftKey: boolean) => void
|
||||
@@ -2110,7 +1930,6 @@ const PositionGapRows = React.memo(
|
||||
startPosition,
|
||||
columns,
|
||||
normalizedSelection,
|
||||
draggingColIndex,
|
||||
checkedRows,
|
||||
firstRowUnderHeader = false,
|
||||
onCellMouseDown,
|
||||
@@ -2176,11 +1995,7 @@ const PositionGapRows = React.memo(
|
||||
key={col.name}
|
||||
data-row={position}
|
||||
data-col={colIndex}
|
||||
className={cn(
|
||||
CELL,
|
||||
(isHighlighted || isAnchor) && 'relative',
|
||||
draggingColIndex === colIndex && 'opacity-40'
|
||||
)}
|
||||
className={cn(CELL, (isHighlighted || isAnchor) && 'relative')}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0) return
|
||||
onCellMouseDown(position, colIndex, e.shiftKey)
|
||||
@@ -2225,7 +2040,6 @@ const PositionGapRows = React.memo(
|
||||
prev.startPosition !== next.startPosition ||
|
||||
prev.columns !== next.columns ||
|
||||
prev.normalizedSelection !== next.normalizedSelection ||
|
||||
prev.draggingColIndex !== next.draggingColIndex ||
|
||||
prev.firstRowUnderHeader !== next.firstRowUnderHeader ||
|
||||
prev.onCellMouseDown !== next.onCellMouseDown ||
|
||||
prev.onCellMouseEnter !== next.onCellMouseEnter ||
|
||||
@@ -2268,7 +2082,6 @@ interface DataRowProps {
|
||||
initialCharacter: string | null
|
||||
pendingCellValue: Record<string, unknown> | null
|
||||
normalizedSelection: NormalizedSelection | null
|
||||
draggingColIndex: number | null
|
||||
onClick: (rowId: string, columnName: string) => void
|
||||
onDoubleClick: (rowId: string, columnName: string) => void
|
||||
onSave: (rowId: string, columnName: string, value: unknown, reason: SaveReason) => void
|
||||
@@ -2319,7 +2132,6 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
|
||||
prev.isFirstRow !== next.isFirstRow ||
|
||||
prev.editingColumnName !== next.editingColumnName ||
|
||||
prev.pendingCellValue !== next.pendingCellValue ||
|
||||
prev.draggingColIndex !== next.draggingColIndex ||
|
||||
prev.onClick !== next.onClick ||
|
||||
prev.onDoubleClick !== next.onDoubleClick ||
|
||||
prev.onSave !== next.onSave ||
|
||||
@@ -2356,7 +2168,6 @@ const DataRow = React.memo(function DataRow({
|
||||
initialCharacter,
|
||||
pendingCellValue,
|
||||
normalizedSelection,
|
||||
draggingColIndex,
|
||||
isRowChecked,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
@@ -2424,11 +2235,7 @@ const DataRow = React.memo(function DataRow({
|
||||
key={column.name}
|
||||
data-row={rowIndex}
|
||||
data-col={colIndex}
|
||||
className={cn(
|
||||
CELL,
|
||||
(isHighlighted || isAnchor || isEditing) && 'relative',
|
||||
draggingColIndex === colIndex && 'opacity-40'
|
||||
)}
|
||||
className={cn(CELL, (isHighlighted || isAnchor || isEditing) && 'relative')}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0 || isEditing) return
|
||||
onCellMouseDown(rowIndex, colIndex, e.shiftKey)
|
||||
@@ -2798,7 +2605,6 @@ const COLUMN_TYPE_OPTIONS: { type: string; label: string; icon: React.ElementTyp
|
||||
|
||||
const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
column,
|
||||
colIndex,
|
||||
readOnly,
|
||||
isRenaming,
|
||||
renameValue,
|
||||
@@ -2815,19 +2621,12 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
isDragging,
|
||||
isDropTarget,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
onDragLeave,
|
||||
sortDirection,
|
||||
onSortAsc,
|
||||
onSortDesc,
|
||||
onFilterColumn,
|
||||
onColumnSelect,
|
||||
}: {
|
||||
column: ColumnDefinition
|
||||
colIndex: number
|
||||
readOnly?: boolean
|
||||
isRenaming: boolean
|
||||
renameValue: string
|
||||
@@ -2844,16 +2643,10 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
onResize: (columnName: string, width: number) => void
|
||||
onResizeEnd: () => void
|
||||
isDragging?: boolean
|
||||
isDropTarget?: boolean
|
||||
onDragStart?: (columnName: string) => void
|
||||
onDragOver?: (columnName: string, side: 'left' | 'right') => void
|
||||
onDragEnd?: () => void
|
||||
onDragLeave?: () => void
|
||||
sortDirection?: SortDirection | null
|
||||
onSortAsc?: (columnName: string) => void
|
||||
onSortDesc?: (columnName: string) => void
|
||||
onFilterColumn?: (columnName: string) => void
|
||||
onColumnSelect?: (colIndex: number) => void
|
||||
}) {
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -2942,8 +2735,7 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
<th
|
||||
className={cn(
|
||||
'group relative border-[var(--border)] border-r border-b bg-[var(--bg)] p-0 text-left align-middle',
|
||||
isDragging && 'opacity-40',
|
||||
isDropTarget && 'bg-[var(--selection)]/10'
|
||||
isDragging && 'opacity-40'
|
||||
)}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
@@ -2968,7 +2760,7 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
) : readOnly ? (
|
||||
<div className='flex h-full w-full min-w-0 items-center px-2 py-[7px]'>
|
||||
<ColumnTypeIcon type={column.type} />
|
||||
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
|
||||
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{column.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -2979,34 +2771,15 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
<button
|
||||
type='button'
|
||||
className='flex min-w-0 flex-1 cursor-pointer items-center px-2 py-[7px] outline-none'
|
||||
onClick={() => onColumnSelect?.(colIndex)}
|
||||
>
|
||||
<ColumnTypeIcon type={column.type} />
|
||||
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
|
||||
{column.name}
|
||||
</span>
|
||||
{sortDirection && (
|
||||
<span className='ml-1 shrink-0'>
|
||||
<SortDirectionIndicator direction={sortDirection} />
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown className='ml-1.5 h-[7px] w-[9px] shrink-0 text-[var(--text-muted)]' />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start'>
|
||||
<DropdownMenuItem onSelect={() => onSortAsc?.(column.name)}>
|
||||
<ArrowUp />
|
||||
Sort ascending
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onSortDesc?.(column.name)}>
|
||||
<ArrowDown />
|
||||
Sort descending
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onFilterColumn?.(column.name)}>
|
||||
<ListFilter />
|
||||
Filter by this column
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={() => onRenameColumn(column.name)}>
|
||||
<Pencil />
|
||||
Rename column
|
||||
@@ -3127,11 +2900,3 @@ function ColumnTypeIcon({ type }: { type: string }) {
|
||||
const Icon = COLUMN_TYPE_ICONS[type] ?? TypeText
|
||||
return <Icon className='h-3 w-3 shrink-0 text-[var(--text-icon)]' />
|
||||
}
|
||||
|
||||
function SortDirectionIndicator({ direction }: { direction: SortDirection }) {
|
||||
return direction === 'asc' ? (
|
||||
<ArrowUp className='h-[10px] w-[10px] text-[var(--text-muted)]' />
|
||||
) : (
|
||||
<ArrowDown className='h-[10px] w-[10px] text-[var(--text-muted)]' />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { createTableColumn, createTableRow } from '@sim/testing'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildTableCsv, formatTableExportValue } from './export'
|
||||
|
||||
describe('table export utils', () => {
|
||||
it('formats exported values using table display conventions', () => {
|
||||
expect(formatTableExportValue('2026-04-03', { name: 'date', type: 'date' })).toBe('04/03/2026')
|
||||
expect(formatTableExportValue({ nested: true }, { name: 'payload', type: 'json' })).toBe(
|
||||
'{"nested":true}'
|
||||
)
|
||||
expect(formatTableExportValue(null, { name: 'empty', type: 'string' })).toBe('')
|
||||
})
|
||||
|
||||
it('builds CSV using visible columns and escaped values', () => {
|
||||
const columns = [
|
||||
createTableColumn({ name: 'name', type: 'string' }),
|
||||
createTableColumn({ name: 'date', type: 'date' }),
|
||||
createTableColumn({ name: 'notes', type: 'json' }),
|
||||
]
|
||||
|
||||
const rows = [
|
||||
createTableRow({
|
||||
id: 'row_1',
|
||||
position: 0,
|
||||
createdAt: '2026-04-03T00:00:00.000Z',
|
||||
updatedAt: '2026-04-03T00:00:00.000Z',
|
||||
data: {
|
||||
name: 'Ada "Lovelace"',
|
||||
date: '2026-04-03',
|
||||
notes: { text: 'line 1\nline 2' },
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
expect(buildTableCsv(columns, rows)).toBe(
|
||||
'name,date,notes\r\n"Ada ""Lovelace""",04/03/2026,"{""text"":""line 1\\nline 2""}"'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { ColumnDefinition, TableRow } from '@/lib/table'
|
||||
import { storageToDisplay } from './utils'
|
||||
|
||||
function safeJsonStringify(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTableExportValue(value: unknown, column: ColumnDefinition): string {
|
||||
if (value === null || value === undefined) return ''
|
||||
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
return storageToDisplay(String(value))
|
||||
case 'json':
|
||||
return typeof value === 'string' ? value : safeJsonStringify(value)
|
||||
default:
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeCsvCell(value: string): string {
|
||||
return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value
|
||||
}
|
||||
|
||||
export function buildTableCsv(columns: ColumnDefinition[], rows: TableRow[]): string {
|
||||
const headerRow = columns.map((column) => escapeCsvCell(column.name)).join(',')
|
||||
const dataRows = rows.map((row) =>
|
||||
columns
|
||||
.map((column) => escapeCsvCell(formatTableExportValue(row.data[column.name], column)))
|
||||
.join(',')
|
||||
)
|
||||
|
||||
return [headerRow, ...dataRows].join('\r\n')
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './use-context-menu'
|
||||
export * from './use-export-table'
|
||||
export * from './use-table-data'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { TableRow } from '@/lib/table'
|
||||
import type { ContextMenuState } from '@/app/workspace/[workspaceId]/tables/[tableId]/types'
|
||||
import type { ContextMenuState } from '../types'
|
||||
|
||||
interface UseContextMenuReturn {
|
||||
contextMenu: ContextMenuState
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { toast } from '@/components/emcn'
|
||||
import { downloadFile, sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
import { buildTableCsv } from '@/app/workspace/[workspaceId]/tables/[tableId]/export'
|
||||
import type { QueryOptions } from '@/app/workspace/[workspaceId]/tables/[tableId]/types'
|
||||
import { fetchAllTableRows } from '@/hooks/queries/tables'
|
||||
|
||||
interface UseExportTableParams {
|
||||
workspaceId: string
|
||||
tableId: string
|
||||
tableName?: string | null
|
||||
columns: ColumnDefinition[]
|
||||
queryOptions: QueryOptions
|
||||
canExport: boolean
|
||||
}
|
||||
|
||||
export function useExportTable({
|
||||
workspaceId,
|
||||
tableId,
|
||||
tableName,
|
||||
columns,
|
||||
queryOptions,
|
||||
canExport,
|
||||
}: UseExportTableParams) {
|
||||
const posthog = usePostHog()
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const isExportingRef = useRef(false)
|
||||
|
||||
const handleExportTable = useCallback(async () => {
|
||||
if (!canExport || !workspaceId || !tableId || isExportingRef.current) return
|
||||
|
||||
isExportingRef.current = true
|
||||
setIsExporting(true)
|
||||
|
||||
try {
|
||||
const { rows } = await fetchAllTableRows({
|
||||
workspaceId,
|
||||
tableId,
|
||||
filter: queryOptions.filter,
|
||||
sort: queryOptions.sort,
|
||||
})
|
||||
|
||||
const filename = `${sanitizePathSegment(tableName?.trim() || 'table')}.csv`
|
||||
const csvContent = buildTableCsv(columns, rows)
|
||||
|
||||
downloadFile(csvContent, filename, 'text/csv;charset=utf-8;')
|
||||
|
||||
captureEvent(posthog, 'table_exported', {
|
||||
workspace_id: workspaceId,
|
||||
table_id: tableId,
|
||||
row_count: rows.length,
|
||||
column_count: columns.length,
|
||||
has_filter: Boolean(queryOptions.filter),
|
||||
has_sort: Boolean(queryOptions.sort),
|
||||
})
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to export table', {
|
||||
duration: 5000,
|
||||
})
|
||||
} finally {
|
||||
isExportingRef.current = false
|
||||
setIsExporting(false)
|
||||
}
|
||||
}, [
|
||||
canExport,
|
||||
columns,
|
||||
posthog,
|
||||
queryOptions.filter,
|
||||
queryOptions.sort,
|
||||
tableId,
|
||||
tableName,
|
||||
workspaceId,
|
||||
])
|
||||
|
||||
return {
|
||||
isExporting,
|
||||
handleExportTable,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { TableDefinition, TableRow } from '@/lib/table'
|
||||
import { TABLE_LIMITS } from '@/lib/table/constants'
|
||||
import type { QueryOptions } from '@/app/workspace/[workspaceId]/tables/[tableId]/types'
|
||||
import { useTable, useTableRows } from '@/hooks/queries/tables'
|
||||
import type { QueryOptions } from '../types'
|
||||
|
||||
interface UseTableDataParams {
|
||||
workspaceId: string
|
||||
@@ -31,7 +30,7 @@ export function useTableData({
|
||||
} = useTableRows({
|
||||
workspaceId,
|
||||
tableId,
|
||||
limit: TABLE_LIMITS.MAX_QUERY_LIMIT,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
filter: queryOptions.filter,
|
||||
sort: queryOptions.sort,
|
||||
|
||||
@@ -68,8 +68,9 @@ export function Tables() {
|
||||
const { data: tables = [], isLoading, error } = useTablesList(workspaceId)
|
||||
const { data: members } = useWorkspaceMembersQuery(workspaceId)
|
||||
|
||||
if (error) logger.error('Failed to load tables:', error)
|
||||
|
||||
if (error) {
|
||||
logger.error('Failed to load tables:', error)
|
||||
}
|
||||
const deleteTable = useDeleteTable(workspaceId)
|
||||
const createTable = useCreateTable(workspaceId)
|
||||
const uploadCsv = useUploadCsvToTable()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { downloadFile, sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { getFolderById } from '@/lib/folders/tree'
|
||||
import {
|
||||
downloadFile,
|
||||
exportFolderToZip,
|
||||
type FolderExportData,
|
||||
fetchWorkflowForExport,
|
||||
sanitizePathSegment,
|
||||
type WorkflowExportData,
|
||||
} from '@/lib/workflows/operations/import-export'
|
||||
import { useFolderMap } from '@/hooks/queries/folders'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { downloadFile } from '@/lib/core/utils/file-download'
|
||||
import {
|
||||
downloadFile,
|
||||
exportWorkflowsToZip,
|
||||
type FolderExportData,
|
||||
fetchWorkflowForExport,
|
||||
|
||||
@@ -2,12 +2,13 @@ import { useCallback, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { downloadFile, sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import {
|
||||
downloadFile,
|
||||
exportWorkflowsToZip,
|
||||
exportWorkflowToJson,
|
||||
fetchWorkflowForExport,
|
||||
sanitizePathSegment,
|
||||
} from '@/lib/workflows/operations/import-export'
|
||||
import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { downloadFile, sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import {
|
||||
downloadFile,
|
||||
exportWorkspaceToZip,
|
||||
type FolderExportData,
|
||||
fetchWorkflowForExport,
|
||||
sanitizePathSegment,
|
||||
type WorkflowExportData,
|
||||
} from '@/lib/workflows/operations/import-export'
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import {
|
||||
extractWorkflowsFromFiles,
|
||||
extractWorkflowsFromZip,
|
||||
persistImportedWorkflow,
|
||||
sanitizePathSegment,
|
||||
} from '@/lib/workflows/operations/import-export'
|
||||
import { useCreateFolder } from '@/hooks/queries/folders'
|
||||
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import {
|
||||
extractWorkflowName,
|
||||
extractWorkflowsFromZip,
|
||||
parseWorkflowJson,
|
||||
sanitizePathSegment,
|
||||
} from '@/lib/workflows/operations/import-export'
|
||||
import { useCreateFolder } from '@/hooks/queries/folders'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
|
||||
329
apps/sim/blocks/blocks/cloudformation.ts
Normal file
329
apps/sim/blocks/blocks/cloudformation.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { CloudFormationIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type {
|
||||
CloudFormationDescribeStackDriftDetectionStatusResponse,
|
||||
CloudFormationDescribeStackEventsResponse,
|
||||
CloudFormationDescribeStacksResponse,
|
||||
CloudFormationDetectStackDriftResponse,
|
||||
CloudFormationGetTemplateResponse,
|
||||
CloudFormationListStackResourcesResponse,
|
||||
CloudFormationValidateTemplateResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
|
||||
export const CloudFormationBlock: BlockConfig<
|
||||
| CloudFormationDescribeStacksResponse
|
||||
| CloudFormationListStackResourcesResponse
|
||||
| CloudFormationDetectStackDriftResponse
|
||||
| CloudFormationDescribeStackDriftDetectionStatusResponse
|
||||
| CloudFormationDescribeStackEventsResponse
|
||||
| CloudFormationGetTemplateResponse
|
||||
| CloudFormationValidateTemplateResponse
|
||||
> = {
|
||||
type: 'cloudformation',
|
||||
name: 'CloudFormation',
|
||||
description: 'Manage and inspect AWS CloudFormation stacks, resources, and drift',
|
||||
longDescription:
|
||||
'Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key.',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['cloud'],
|
||||
docsLink: 'https://docs.sim.ai/tools/cloudformation',
|
||||
bgColor: 'linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)',
|
||||
icon: CloudFormationIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Describe Stacks', id: 'describe_stacks' },
|
||||
{ label: 'List Stack Resources', id: 'list_stack_resources' },
|
||||
{ label: 'Describe Stack Events', id: 'describe_stack_events' },
|
||||
{ label: 'Detect Stack Drift', id: 'detect_stack_drift' },
|
||||
{ label: 'Drift Detection Status', id: 'describe_stack_drift_detection_status' },
|
||||
{ label: 'Get Template', id: 'get_template' },
|
||||
{ label: 'Validate Template', id: 'validate_template' },
|
||||
],
|
||||
value: () => 'describe_stacks',
|
||||
},
|
||||
{
|
||||
id: 'awsRegion',
|
||||
title: 'AWS Region',
|
||||
type: 'short-input',
|
||||
placeholder: 'us-east-1',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'awsAccessKeyId',
|
||||
title: 'AWS Access Key ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'AKIA...',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'awsSecretAccessKey',
|
||||
title: 'AWS Secret Access Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Your secret access key',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'stackName',
|
||||
title: 'Stack Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'my-stack or arn:aws:cloudformation:...',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'describe_stacks',
|
||||
'list_stack_resources',
|
||||
'describe_stack_events',
|
||||
'detect_stack_drift',
|
||||
'get_template',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_stack_resources',
|
||||
'describe_stack_events',
|
||||
'detect_stack_drift',
|
||||
'get_template',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stackDriftDetectionId',
|
||||
title: 'Drift Detection ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'ID from Detect Stack Drift output',
|
||||
condition: { field: 'operation', value: 'describe_stack_drift_detection_status' },
|
||||
required: { field: 'operation', value: 'describe_stack_drift_detection_status' },
|
||||
},
|
||||
{
|
||||
id: 'templateBody',
|
||||
title: 'Template Body',
|
||||
type: 'code',
|
||||
placeholder: '{\n "AWSTemplateFormatVersion": "2010-09-09",\n "Resources": { ... }\n}',
|
||||
condition: { field: 'operation', value: 'validate_template' },
|
||||
required: { field: 'operation', value: 'validate_template' },
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '50',
|
||||
condition: { field: 'operation', value: 'describe_stack_events' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'cloudformation_describe_stacks',
|
||||
'cloudformation_list_stack_resources',
|
||||
'cloudformation_detect_stack_drift',
|
||||
'cloudformation_describe_stack_drift_detection_status',
|
||||
'cloudformation_describe_stack_events',
|
||||
'cloudformation_get_template',
|
||||
'cloudformation_validate_template',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'describe_stacks':
|
||||
return 'cloudformation_describe_stacks'
|
||||
case 'list_stack_resources':
|
||||
return 'cloudformation_list_stack_resources'
|
||||
case 'detect_stack_drift':
|
||||
return 'cloudformation_detect_stack_drift'
|
||||
case 'describe_stack_drift_detection_status':
|
||||
return 'cloudformation_describe_stack_drift_detection_status'
|
||||
case 'describe_stack_events':
|
||||
return 'cloudformation_describe_stack_events'
|
||||
case 'get_template':
|
||||
return 'cloudformation_get_template'
|
||||
case 'validate_template':
|
||||
return 'cloudformation_validate_template'
|
||||
default:
|
||||
throw new Error(`Invalid CloudFormation operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, limit, ...rest } = params
|
||||
|
||||
const awsRegion = rest.awsRegion
|
||||
const awsAccessKeyId = rest.awsAccessKeyId
|
||||
const awsSecretAccessKey = rest.awsSecretAccessKey
|
||||
const parsedLimit = limit ? Number.parseInt(String(limit), 10) : undefined
|
||||
|
||||
switch (operation) {
|
||||
case 'describe_stacks':
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
...(rest.stackName && { stackName: rest.stackName }),
|
||||
}
|
||||
|
||||
case 'list_stack_resources': {
|
||||
if (!rest.stackName) {
|
||||
throw new Error('Stack name is required')
|
||||
}
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
stackName: rest.stackName,
|
||||
}
|
||||
}
|
||||
|
||||
case 'detect_stack_drift': {
|
||||
if (!rest.stackName) {
|
||||
throw new Error('Stack name is required')
|
||||
}
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
stackName: rest.stackName,
|
||||
}
|
||||
}
|
||||
|
||||
case 'describe_stack_drift_detection_status': {
|
||||
if (!rest.stackDriftDetectionId) {
|
||||
throw new Error('Drift detection ID is required')
|
||||
}
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
stackDriftDetectionId: rest.stackDriftDetectionId,
|
||||
}
|
||||
}
|
||||
|
||||
case 'describe_stack_events': {
|
||||
if (!rest.stackName) {
|
||||
throw new Error('Stack name is required')
|
||||
}
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
stackName: rest.stackName,
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_template': {
|
||||
if (!rest.stackName) {
|
||||
throw new Error('Stack name is required')
|
||||
}
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
stackName: rest.stackName,
|
||||
}
|
||||
}
|
||||
|
||||
case 'validate_template': {
|
||||
if (!rest.templateBody) {
|
||||
throw new Error('Template body is required')
|
||||
}
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
templateBody: rest.templateBody,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid CloudFormation operation: ${operation}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'CloudFormation operation to perform' },
|
||||
awsRegion: { type: 'string', description: 'AWS region' },
|
||||
awsAccessKeyId: { type: 'string', description: 'AWS access key ID' },
|
||||
awsSecretAccessKey: { type: 'string', description: 'AWS secret access key' },
|
||||
stackName: { type: 'string', description: 'Stack name or ID' },
|
||||
stackDriftDetectionId: { type: 'string', description: 'Drift detection ID' },
|
||||
templateBody: { type: 'string', description: 'CloudFormation template body (JSON or YAML)' },
|
||||
limit: { type: 'number', description: 'Maximum number of results' },
|
||||
},
|
||||
outputs: {
|
||||
stacks: {
|
||||
type: 'array',
|
||||
description: 'List of CloudFormation stacks with status, outputs, and tags',
|
||||
},
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'List of stack resources with type, status, and drift info',
|
||||
},
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'Stack events with resource status and timestamps',
|
||||
},
|
||||
stackDriftDetectionId: {
|
||||
type: 'string',
|
||||
description: 'Drift detection ID for checking status',
|
||||
},
|
||||
stackId: {
|
||||
type: 'string',
|
||||
description: 'Stack ID',
|
||||
},
|
||||
stackDriftStatus: {
|
||||
type: 'string',
|
||||
description: 'Drift status (DRIFTED, IN_SYNC, NOT_CHECKED)',
|
||||
},
|
||||
detectionStatus: {
|
||||
type: 'string',
|
||||
description: 'Detection status (DETECTION_IN_PROGRESS, DETECTION_COMPLETE, DETECTION_FAILED)',
|
||||
},
|
||||
detectionStatusReason: {
|
||||
type: 'string',
|
||||
description: 'Reason if detection failed',
|
||||
},
|
||||
driftedStackResourceCount: {
|
||||
type: 'number',
|
||||
description: 'Number of drifted resources',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'number',
|
||||
description: 'Detection timestamp',
|
||||
},
|
||||
templateBody: {
|
||||
type: 'string',
|
||||
description: 'Template body (JSON or YAML)',
|
||||
},
|
||||
stagesAvailable: {
|
||||
type: 'array',
|
||||
description: 'Available template stages',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Template description',
|
||||
},
|
||||
parameters: {
|
||||
type: 'array',
|
||||
description: 'Template parameters',
|
||||
},
|
||||
capabilities: {
|
||||
type: 'array',
|
||||
description: 'Required capabilities',
|
||||
},
|
||||
capabilitiesReason: {
|
||||
type: 'string',
|
||||
description: 'Reason capabilities are required',
|
||||
},
|
||||
declaredTransforms: {
|
||||
type: 'array',
|
||||
description: 'Transforms used in the template (e.g., AWS::Serverless-2016-10-31)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { CirclebackBlock } from '@/blocks/blocks/circleback'
|
||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||
import { ClerkBlock } from '@/blocks/blocks/clerk'
|
||||
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
|
||||
import { CloudFormationBlock } from '@/blocks/blocks/cloudformation'
|
||||
import { CloudWatchBlock } from '@/blocks/blocks/cloudwatch'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
||||
@@ -242,6 +243,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
chat_trigger: ChatTriggerBlock,
|
||||
circleback: CirclebackBlock,
|
||||
cloudflare: CloudflareBlock,
|
||||
cloudformation: CloudFormationBlock,
|
||||
cloudwatch: CloudWatchBlock,
|
||||
clay: ClayBlock,
|
||||
clerk: ClerkBlock,
|
||||
|
||||
@@ -51,13 +51,6 @@ import { Button } from '../button/button'
|
||||
const ANIMATION_CLASSES =
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'
|
||||
|
||||
/**
|
||||
* Modal content animation classes.
|
||||
* We keep only the slide animations (no zoom) to stabilize positioning while avoiding scale effects.
|
||||
*/
|
||||
const CONTENT_ANIMATION_CLASSES =
|
||||
'data-[state=closed]:slide-out-to-top-[50%] data-[state=open]:slide-in-from-top-[50%] motion-reduce:animate-none'
|
||||
|
||||
/**
|
||||
* Root modal component. Manages open state.
|
||||
*/
|
||||
@@ -166,7 +159,8 @@ const ModalContent = React.forwardRef<
|
||||
)}
|
||||
style={{
|
||||
left: isWorkflowPage
|
||||
? 'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)'
|
||||
? // --panel-width is always the rendered panel width on /w/ routes (panel is never hidden/collapsed)
|
||||
'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)'
|
||||
: 'calc(var(--sidebar-width) / 2 + 50%)',
|
||||
...style,
|
||||
}}
|
||||
|
||||
@@ -4653,6 +4653,32 @@ export function SQSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudFormationIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 80 80'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
>
|
||||
<g
|
||||
id='Icon-Architecture/64/Arch_AWS-CloudFormation_64'
|
||||
stroke='none'
|
||||
strokeWidth='1'
|
||||
fill='none'
|
||||
fillRule='evenodd'
|
||||
>
|
||||
<path
|
||||
d='M53,39.9632039 L58,39.9632039 L58,37.9601375 L53,37.9601375 L53,39.9632039 Z M28,51.9816019 L33,51.9816019 L33,49.9785356 L28,49.9785356 L28,51.9816019 Z M18,51.9816019 L25,51.9816019 L25,49.9785356 L18,49.9785356 L18,51.9816019 Z M18,45.9724029 L30,45.9724029 L30,43.9693366 L18,43.9693366 L18,45.9724029 Z M18,33.9540048 L27,33.9540048 L27,31.9509385 L18,31.9509385 L18,33.9540048 Z M18,39.9632039 L51,39.9632039 L51,37.9601375 L18,37.9601375 L18,39.9632039 Z M37,61.9969337 L14,61.9969337 L14,27.9448058 L37,27.9448058 L37,35.9570712 L39,35.9570712 L39,26.9432726 C39,26.3904263 38.552,25.9417395 38,25.9417395 L13,25.9417395 C12.447,25.9417395 12,26.3904263 12,26.9432726 L12,62.9984668 C12,63.5513131 12.447,64 13,64 L38,64 C38.552,64 39,63.5513131 39,62.9984668 L39,42.9678034 L37,42.9678034 L37,61.9969337 Z M68,36.9586044 C68,43.4305117 62.173,45.6819583 59.092,45.9683968 L43,45.9724029 L43,43.9693366 L59,43.9693366 C59.195,43.9463013 66,43.2121775 66,36.9586044 C66,31.2638867 60.863,30.1081175 59.834,29.9338507 C59.321,29.8467173 58.96,29.3820059 59.004,28.8632117 C59.005,28.8441826 59.007,28.826155 59.009,28.8081274 C58.954,25.5902013 56.981,24.584662 56.126,24.3002266 C54.53,23.769414 52.751,24.2771913 51.81,25.5391231 C51.591,25.8355769 51.229,25.9868085 50.861,25.9307226 C50.497,25.8756383 50.192,25.625255 50.068,25.2767214 C49.447,23.5360568 48.546,22.4083304 47.293,21.1534094 C44.159,18.0386412 39.905,17.1783242 35.925,18.8528877 C33.837,19.7332353 32.012,21.7282894 30.922,24.327268 L29.078,23.5500782 C30.37,20.4743699 32.584,18.0887179 35.15,17.007062 C39.905,15.0049972 44.971,16.0255595 48.704,19.7342369 C49.774,20.8068789 50.66,21.851478 51.35,23.2035478 C52.843,22.0978551 54.857,21.7673492 56.757,22.3993166 C59.189,23.2085554 60.727,25.3207889 60.975,28.1290879 C64.381,28.9884034 68,31.7115721 68,36.9586044 L68,36.9586044 Z'
|
||||
id='AWS-CloudFormation_Icon_64_Squid'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudWatchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4671,7 +4697,7 @@ export function CloudWatchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
transform='translate(40, 40) scale(1.25) translate(-40, -40)'
|
||||
>
|
||||
<path
|
||||
d='M53,42 L41,42 L41,24 L43,24 L43,40 L53,40 L53,42 Z M40,66 C24.561,66 12,53.439 12,38 C12,22.561 24.561,10 40,10 C55.439,10 68,22.561 68,38 C68,53.439 55.439,66 40,66 M40,8 C23.458,8 10,21.458 10,38 C10,54.542 23.458,68 40,68 C56.542,68 70,54.542 70,38 C70,21.458 56.542,8 40,8'
|
||||
d='M55.0592315,46.7773707 C55.0592315,42.8680281 51.8575588,39.6876305 47.9220646,39.6876305 C43.9865705,39.6876305 40.785903,42.8680281 40.785903,46.7773707 C40.785903,50.6867133 43.9865705,53.8671108 47.9220646,53.8671108 C51.8575588,53.8671108 55.0592315,50.6867133 55.0592315,46.7773707 M57.0697011,46.7773707 C57.0697011,51.7881194 52.9663327,55.8642207 47.9220646,55.8642207 C42.8788018,55.8642207 38.7754334,51.7881194 38.7754334,46.7773707 C38.7754334,41.7666219 42.8788018,37.6905206 47.9220646,37.6905206 C52.9663327,37.6905206 57.0697011,41.7666219 57.0697011,46.7773707 M65.5096522,60.4735504 L58.5011554,54.2026253 C57.9352082,54.9944794 57.2808004,55.7174332 56.5540156,56.3634982 L63.5524601,62.6334248 C64.1495696,63.1686502 65.0784065,63.1187225 65.6182176,62.5255808 C66.155013,61.9324392 66.1067617,61.010773 65.5096522,60.4735504 M47.9220646,57.6616197 C53.9645309,57.6616197 58.8801289,52.7786859 58.8801289,46.7773707 C58.8801289,40.7750569 53.9645309,35.8931217 47.9220646,35.8931217 C41.8806036,35.8931217 36.9650056,40.7750569 36.9650056,46.7773707 C36.9650056,52.7786859 41.8806036,57.6616197 47.9220646,57.6616197 M67.1119965,63.8626459 C66.4264264,64.6165549 65.47849,65 64.5285431,65 C63.7002296,65 62.8699057,64.708422 62.207456,64.1172774 L54.9305615,57.5987107 C52.9070239,58.8968321 50.505518,59.6587296 47.9220646,59.6587296 C40.7718297,59.6587296 34.9545361,53.8800921 34.9545361,46.7773707 C34.9545361,39.6746493 40.7718297,33.8960118 47.9220646,33.8960118 C55.0733048,33.8960118 60.8905985,39.6746493 60.8905985,46.7773707 C60.8905985,48.8154213 60.3990387,50.7366411 59.5465996,52.4511599 L66.8556616,58.9906963 C68.2750531,60.265851 68.3896499,62.4496907 67.1119965,63.8626459 M21.2803274,29.392529 C21.2803274,29.9117776 21.3124949,30.429029 21.3738143,30.9293051 C21.4089975,31.2138932 21.3205368,31.4984814 21.1295422,31.7131707 C20.9777518,31.8839236 20.7736891,31.9967603 20.550527,32.0347054 C18.0786547,32.6687878 14.0104695,34.5880104 14.0104695,40.3456782 C14.0104695,44.6933865 16.4240382,47.0929141 18.4495863,48.3411077 C19.1411878,48.7744806 19.9594489,49.0051468 20.8229456,49.0141338 L32.9450717,49.0251179 L32.9430613,51.0222278 L20.811888,51.0112437 C19.5664021,50.9982625 18.384246,50.6607509 17.3840374,50.0346569 C15.3765836,48.7974474 12,45.8896553 12,40.3456782 C12,33.66235 16.5999543,31.191925 19.3000149,30.319188 C19.2799102,30.0116331 19.2698579,29.702081 19.2698579,29.392529 C19.2698579,23.9324305 22.9982737,18.2696254 27.9420183,16.2215892 C33.7241287,13.8150717 39.8500294,15.0083449 44.3263399,19.4109737 C45.7135638,20.7749998 46.8545053,22.4316024 47.7300648,24.3478294 C48.9061895,23.3802296 50.355738,22.8460027 51.8836949,22.8460027 C54.8863312,22.8460027 58.2659305,25.1097268 58.8680661,30.0605622 C61.6797078,30.7046302 67.6206453,32.9553731 67.6206453,40.422567 C67.6206453,43.4042521 66.6797455,45.8666886 64.8230769,47.7419748 L63.3896121,46.3410022 C64.8632863,44.8531553 65.6101757,42.8620367 65.6101757,40.422567 C65.6101757,33.891019 60.1055101,32.2663701 57.737177,31.8719409 C57.4677741,31.827006 57.2295334,31.6752256 57.0757325,31.4515493 C56.9259525,31.2358614 56.8686541,30.9712444 56.9138897,30.7146157 C56.5851779,26.6604826 54.1605516,24.8431126 51.8836949,24.8431126 C50.4472144,24.8431126 49.1001998,25.5381069 48.1874466,26.7503526 C47.9652897,27.0439277 47.6044105,27.193711 47.2344841,27.139789 C46.8695838,27.085867 46.5629872,26.8362283 46.4373329,26.4917268 C45.6140456,24.2260057 44.4278686,22.3207628 42.9119745,20.8309188 C39.0327735,17.0154404 33.7281496,15.9809374 28.7170543,18.0649216 C24.5463352,19.7924217 21.2803274,24.7672224 21.2803274,29.392529'
|
||||
id='Amazon-CloudWatch_Icon_64_Squid'
|
||||
fill='currentColor'
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createLogger } from '@sim/logger'
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from '@/components/emcn'
|
||||
import type { Filter, RowData, Sort, TableDefinition, TableMetadata, TableRow } from '@/lib/table'
|
||||
import { TABLE_LIMITS } from '@/lib/table/constants'
|
||||
|
||||
const logger = createLogger('TableQueries')
|
||||
|
||||
@@ -24,7 +23,7 @@ export const tableKeys = {
|
||||
[...tableKeys.rowsRoot(tableId), paramsKey] as const,
|
||||
}
|
||||
|
||||
export interface TableRowsParams {
|
||||
interface TableRowsParams {
|
||||
workspaceId: string
|
||||
tableId: string
|
||||
limit: number
|
||||
@@ -33,7 +32,7 @@ export interface TableRowsParams {
|
||||
sort?: Sort | null
|
||||
}
|
||||
|
||||
export interface TableRowsResponse {
|
||||
interface TableRowsResponse {
|
||||
rows: TableRow[]
|
||||
totalCount: number
|
||||
}
|
||||
@@ -84,7 +83,7 @@ async function fetchTable(
|
||||
return (data as { table: TableDefinition }).table
|
||||
}
|
||||
|
||||
export async function fetchTableRows({
|
||||
async function fetchTableRows({
|
||||
workspaceId,
|
||||
tableId,
|
||||
limit,
|
||||
@@ -126,48 +125,6 @@ export async function fetchTableRows({
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllTableRows({
|
||||
workspaceId,
|
||||
tableId,
|
||||
filter,
|
||||
sort,
|
||||
pageSize = TABLE_LIMITS.MAX_QUERY_LIMIT,
|
||||
signal,
|
||||
}: Pick<TableRowsParams, 'workspaceId' | 'tableId' | 'filter' | 'sort'> & {
|
||||
pageSize?: number
|
||||
signal?: AbortSignal
|
||||
}): Promise<TableRowsResponse> {
|
||||
const rows: TableRow[] = []
|
||||
let totalCount = Number.POSITIVE_INFINITY
|
||||
let offset = 0
|
||||
|
||||
while (rows.length < totalCount) {
|
||||
const response = await fetchTableRows({
|
||||
workspaceId,
|
||||
tableId,
|
||||
limit: pageSize,
|
||||
offset,
|
||||
filter,
|
||||
sort,
|
||||
signal,
|
||||
})
|
||||
|
||||
rows.push(...response.rows)
|
||||
totalCount = response.totalCount
|
||||
|
||||
if (response.rows.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
offset += response.rows.length
|
||||
}
|
||||
|
||||
return {
|
||||
rows,
|
||||
totalCount: Number.isFinite(totalCount) ? totalCount : rows.length,
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateRowData(queryClient: ReturnType<typeof useQueryClient>, tableId: string) {
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) })
|
||||
}
|
||||
|
||||
@@ -191,21 +191,6 @@ export function useTableUndo({ workspaceId, tableId }: UseTableUndoProps) {
|
||||
break
|
||||
}
|
||||
|
||||
case 'delete-column': {
|
||||
if (direction === 'undo') {
|
||||
addColumnMutation.mutate({
|
||||
name: action.columnName,
|
||||
type: action.columnType,
|
||||
position: action.position,
|
||||
unique: action.unique,
|
||||
required: action.required,
|
||||
})
|
||||
} else {
|
||||
deleteColumnMutation.mutate(action.columnName)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'rename-column': {
|
||||
if (direction === 'undo') {
|
||||
updateColumnMutation.mutate({
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createEditWorkflowRegistryMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createBlockFromParams } from './builders'
|
||||
|
||||
vi.mock('@/blocks/registry', () => createEditWorkflowRegistryMock(['agent', 'condition']))
|
||||
const agentBlockConfig = {
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Default content output' },
|
||||
},
|
||||
subBlocks: [{ id: 'responseFormat', type: 'response-format' }],
|
||||
}
|
||||
|
||||
const conditionBlockConfig = {
|
||||
type: 'condition',
|
||||
name: 'Condition',
|
||||
outputs: {},
|
||||
subBlocks: [{ id: 'conditions', type: 'condition-input' }],
|
||||
}
|
||||
|
||||
vi.mock('@/blocks/registry', () => ({
|
||||
getAllBlocks: () => [agentBlockConfig, conditionBlockConfig],
|
||||
getBlock: (type: string) =>
|
||||
type === 'agent' ? agentBlockConfig : type === 'condition' ? conditionBlockConfig : undefined,
|
||||
}))
|
||||
|
||||
describe('createBlockFromParams', () => {
|
||||
it('derives agent outputs from responseFormat when outputs are not provided', () => {
|
||||
|
||||
@@ -1,16 +1,69 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createEditWorkflowRegistryMock } from '@sim/testing'
|
||||
import { loggerMock } from '@sim/testing/mocks'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { applyOperationsToWorkflowState } from './engine'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/blocks/registry', () =>
|
||||
createEditWorkflowRegistryMock(['condition', 'agent', 'function'])
|
||||
)
|
||||
vi.mock('@/blocks/registry', () => ({
|
||||
getAllBlocks: () => [
|
||||
{
|
||||
type: 'condition',
|
||||
name: 'Condition',
|
||||
subBlocks: [{ id: 'conditions', type: 'condition-input' }],
|
||||
},
|
||||
{
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
subBlocks: [
|
||||
{ id: 'systemPrompt', type: 'long-input' },
|
||||
{ id: 'model', type: 'combobox' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'Function',
|
||||
subBlocks: [
|
||||
{ id: 'code', type: 'code' },
|
||||
{ id: 'language', type: 'dropdown' },
|
||||
],
|
||||
},
|
||||
],
|
||||
getBlock: (type: string) => {
|
||||
const blocks: Record<string, any> = {
|
||||
condition: {
|
||||
type: 'condition',
|
||||
name: 'Condition',
|
||||
subBlocks: [{ id: 'conditions', type: 'condition-input' }],
|
||||
},
|
||||
agent: {
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
subBlocks: [
|
||||
{ id: 'systemPrompt', type: 'long-input' },
|
||||
{ id: 'model', type: 'combobox' },
|
||||
],
|
||||
},
|
||||
function: {
|
||||
type: 'function',
|
||||
name: 'Function',
|
||||
subBlocks: [
|
||||
{ id: 'code', type: 'code' },
|
||||
{ id: 'language', type: 'dropdown' },
|
||||
],
|
||||
},
|
||||
}
|
||||
return blocks[type] || undefined
|
||||
},
|
||||
}))
|
||||
|
||||
function makeLoopWorkflow() {
|
||||
return {
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createEditWorkflowRegistryMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { normalizeConditionRouterIds } from './builders'
|
||||
import { validateInputsForBlock } from './validation'
|
||||
|
||||
vi.mock('@/blocks/registry', () => createEditWorkflowRegistryMock(['condition', 'router_v2']))
|
||||
const conditionBlockConfig = {
|
||||
type: 'condition',
|
||||
name: 'Condition',
|
||||
outputs: {},
|
||||
subBlocks: [{ id: 'conditions', type: 'condition-input' }],
|
||||
}
|
||||
|
||||
const routerBlockConfig = {
|
||||
type: 'router_v2',
|
||||
name: 'Router',
|
||||
outputs: {},
|
||||
subBlocks: [{ id: 'routes', type: 'router-input' }],
|
||||
}
|
||||
|
||||
vi.mock('@/blocks/registry', () => ({
|
||||
getBlock: (type: string) =>
|
||||
type === 'condition'
|
||||
? conditionBlockConfig
|
||||
: type === 'router_v2'
|
||||
? routerBlockConfig
|
||||
: undefined,
|
||||
}))
|
||||
|
||||
describe('validateInputsForBlock', () => {
|
||||
it('accepts condition-input arrays with arbitrary item ids', () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createFeatureFlagsMock, loggerMock } from '@sim/testing'
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { RateLimiter } from './rate-limiter'
|
||||
import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './storage'
|
||||
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
vi.mock('@/lib/core/config/feature-flags', () => createFeatureFlagsMock({ isBillingEnabled: true }))
|
||||
vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true }))
|
||||
|
||||
interface MockAdapter {
|
||||
consumeTokens: Mock
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
|
||||
const logger = createLogger('FileDownload')
|
||||
|
||||
/**
|
||||
* Sanitizes a string for use as a file or path segment in exported assets.
|
||||
*/
|
||||
export function sanitizePathSegment(name: string): string {
|
||||
return name.replace(/[^a-z0-9-_]/gi, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file to the user's device.
|
||||
* Throws if the browser cannot create or trigger the download.
|
||||
*/
|
||||
export function downloadFile(
|
||||
content: Blob | string,
|
||||
filename: string,
|
||||
mimeType = 'application/json'
|
||||
): void {
|
||||
try {
|
||||
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
logger.error('Failed to download file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createFeatureFlagsMock, loggerMock } from '@sim/testing'
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
interface MockMcpClient {
|
||||
@@ -38,7 +38,7 @@ const { MockMcpClientConstructor, mockOnToolsChanged, mockPublishToolsChanged }
|
||||
)
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
vi.mock('@/lib/core/config/feature-flags', () => createFeatureFlagsMock({ isTest: false }))
|
||||
vi.mock('@/lib/core/config/feature-flags', () => ({ isTest: false }))
|
||||
vi.mock('@/lib/mcp/pubsub', () => ({
|
||||
mcpPubSub: {
|
||||
onToolsChanged: mockOnToolsChanged,
|
||||
|
||||
@@ -317,15 +317,6 @@ export interface PostHogEventMap {
|
||||
workspace_id: string
|
||||
}
|
||||
|
||||
table_exported: {
|
||||
workspace_id: string
|
||||
table_id: string
|
||||
row_count: number
|
||||
column_count: number
|
||||
has_filter: boolean
|
||||
has_sort: boolean
|
||||
}
|
||||
|
||||
custom_tool_saved: {
|
||||
tool_id: string
|
||||
workspace_id: string
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createTableColumn } from '@sim/testing'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { TABLE_LIMITS } from '../constants'
|
||||
import {
|
||||
type ColumnDefinition,
|
||||
getUniqueColumns,
|
||||
type TableSchema,
|
||||
validateColumnDefinition,
|
||||
@@ -66,12 +66,12 @@ describe('Validation', () => {
|
||||
|
||||
describe('validateColumnDefinition', () => {
|
||||
it('should accept valid column definition', () => {
|
||||
const column = createTableColumn({
|
||||
const column: ColumnDefinition = {
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
required: true,
|
||||
unique: true,
|
||||
})
|
||||
}
|
||||
const result = validateColumnDefinition(column)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
@@ -80,20 +80,19 @@ describe('Validation', () => {
|
||||
const types = ['string', 'number', 'boolean', 'date', 'json'] as const
|
||||
|
||||
for (const type of types) {
|
||||
const result = validateColumnDefinition(createTableColumn({ name: 'test', type }))
|
||||
const result = validateColumnDefinition({ name: 'test', type })
|
||||
expect(result.valid).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject empty column name', () => {
|
||||
const result = validateColumnDefinition(createTableColumn({ name: '', type: 'string' }))
|
||||
const result = validateColumnDefinition({ name: '', type: 'string' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('Column name is required')
|
||||
})
|
||||
|
||||
it('should reject invalid column type', () => {
|
||||
const result = validateColumnDefinition({
|
||||
...createTableColumn({ name: 'test' }),
|
||||
name: 'test',
|
||||
type: 'invalid' as any,
|
||||
})
|
||||
@@ -103,7 +102,7 @@ describe('Validation', () => {
|
||||
|
||||
it('should reject column name exceeding max length', () => {
|
||||
const longName = 'a'.repeat(TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH + 1)
|
||||
const result = validateColumnDefinition(createTableColumn({ name: longName, type: 'string' }))
|
||||
const result = validateColumnDefinition({ name: longName, type: 'string' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[0]).toContain('exceeds maximum length')
|
||||
})
|
||||
@@ -113,9 +112,9 @@ describe('Validation', () => {
|
||||
it('should accept valid schema', () => {
|
||||
const schema: TableSchema = {
|
||||
columns: [
|
||||
createTableColumn({ name: 'id', type: 'string', required: true, unique: true }),
|
||||
createTableColumn({ name: 'name', type: 'string', required: true }),
|
||||
createTableColumn({ name: 'age', type: 'number' }),
|
||||
{ name: 'id', type: 'string', required: true, unique: true },
|
||||
{ name: 'name', type: 'string', required: true },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
}
|
||||
const result = validateTableSchema(schema)
|
||||
@@ -132,8 +131,8 @@ describe('Validation', () => {
|
||||
it('should reject duplicate column names', () => {
|
||||
const schema: TableSchema = {
|
||||
columns: [
|
||||
createTableColumn({ name: 'id', type: 'string' }),
|
||||
createTableColumn({ name: 'ID', type: 'number' }),
|
||||
{ name: 'id', type: 'string' },
|
||||
{ name: 'ID', type: 'number' },
|
||||
],
|
||||
}
|
||||
const result = validateTableSchema(schema)
|
||||
@@ -154,9 +153,10 @@ describe('Validation', () => {
|
||||
})
|
||||
|
||||
it('should reject schema exceeding max columns', () => {
|
||||
const columns = Array.from({ length: TABLE_LIMITS.MAX_COLUMNS_PER_TABLE + 1 }, (_, i) =>
|
||||
createTableColumn({ name: `col_${i}`, type: 'string' })
|
||||
)
|
||||
const columns = Array.from({ length: TABLE_LIMITS.MAX_COLUMNS_PER_TABLE + 1 }, (_, i) => ({
|
||||
name: `col_${i}`,
|
||||
type: 'string' as const,
|
||||
}))
|
||||
const result = validateTableSchema({ columns })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[0]).toContain('exceeds maximum columns')
|
||||
@@ -182,11 +182,11 @@ describe('Validation', () => {
|
||||
describe('validateRowAgainstSchema', () => {
|
||||
const schema: TableSchema = {
|
||||
columns: [
|
||||
createTableColumn({ name: 'name', type: 'string', required: true }),
|
||||
createTableColumn({ name: 'age', type: 'number' }),
|
||||
createTableColumn({ name: 'active', type: 'boolean' }),
|
||||
createTableColumn({ name: 'created', type: 'date' }),
|
||||
createTableColumn({ name: 'metadata', type: 'json' }),
|
||||
{ name: 'name', type: 'string', required: true },
|
||||
{ name: 'age', type: 'number' },
|
||||
{ name: 'active', type: 'boolean' },
|
||||
{ name: 'created', type: 'date' },
|
||||
{ name: 'metadata', type: 'json' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -281,10 +281,10 @@ describe('Validation', () => {
|
||||
it('should return only columns with unique=true', () => {
|
||||
const schema: TableSchema = {
|
||||
columns: [
|
||||
createTableColumn({ name: 'id', type: 'string', unique: true }),
|
||||
createTableColumn({ name: 'email', type: 'string', unique: true }),
|
||||
createTableColumn({ name: 'name', type: 'string' }),
|
||||
createTableColumn({ name: 'count', type: 'number', unique: false }),
|
||||
{ name: 'id', type: 'string', unique: true },
|
||||
{ name: 'email', type: 'string', unique: true },
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'count', type: 'number', unique: false },
|
||||
],
|
||||
}
|
||||
const result = getUniqueColumns(schema)
|
||||
@@ -295,8 +295,8 @@ describe('Validation', () => {
|
||||
it('should return empty array when no unique columns', () => {
|
||||
const schema: TableSchema = {
|
||||
columns: [
|
||||
createTableColumn({ name: 'name', type: 'string' }),
|
||||
createTableColumn({ name: 'value', type: 'number' }),
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'value', type: 'number' },
|
||||
],
|
||||
}
|
||||
const result = getUniqueColumns(schema)
|
||||
@@ -307,9 +307,9 @@ describe('Validation', () => {
|
||||
describe('validateUniqueConstraints', () => {
|
||||
const schema: TableSchema = {
|
||||
columns: [
|
||||
createTableColumn({ name: 'id', type: 'string', unique: true }),
|
||||
createTableColumn({ name: 'email', type: 'string', unique: true }),
|
||||
createTableColumn({ name: 'name', type: 'string' }),
|
||||
{ name: 'id', type: 'string', unique: true },
|
||||
{ name: 'email', type: 'string', unique: true },
|
||||
{ name: 'name', type: 'string' },
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
/**
|
||||
* Tests for workflow change detection comparison logic
|
||||
*/
|
||||
|
||||
import type { WorkflowVariableFixture } from '@sim/testing'
|
||||
import {
|
||||
createBlock as createTestBlock,
|
||||
createWorkflowState as createTestWorkflowState,
|
||||
createWorkflowVariablesMap,
|
||||
} from '@sim/testing'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
@@ -49,12 +46,6 @@ function createBlock(id: string, overrides: Record<string, any> = {}): any {
|
||||
})
|
||||
}
|
||||
|
||||
function createVariablesMap(
|
||||
...variables: Parameters<typeof createWorkflowVariablesMap>[0]
|
||||
): Record<string, WorkflowVariableFixture> {
|
||||
return createWorkflowVariablesMap(variables)
|
||||
}
|
||||
|
||||
describe('hasWorkflowChanged', () => {
|
||||
describe('Basic Cases', () => {
|
||||
it.concurrent('should return true when deployedState is null', () => {
|
||||
@@ -2190,12 +2181,9 @@ describe('hasWorkflowChanged', () => {
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'myVar',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
@@ -2204,12 +2192,9 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should detect removed variables', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'myVar',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
@@ -2223,22 +2208,16 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should detect variable value changes', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'myVar',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'myVar',
|
||||
type: 'string',
|
||||
value: 'world',
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'world' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
@@ -2247,12 +2226,16 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should detect variable type changes', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({ id: 'var1', name: 'myVar', type: 'string', value: '123' }),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: '123' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({ id: 'var1', name: 'myVar', type: 'number', value: 123 }),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'number', value: 123 },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
@@ -2261,22 +2244,16 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should detect variable name changes', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'oldName',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'oldName', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'newName',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'newName', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
@@ -2285,18 +2262,18 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should not detect change for identical variables', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap(
|
||||
{ id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
{ id: 'var2', name: 'count', type: 'number', value: 42 }
|
||||
),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap(
|
||||
{ id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
{ id: 'var2', name: 'count', type: 'number', value: 42 }
|
||||
),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false)
|
||||
@@ -2333,22 +2310,16 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should handle complex variable values (objects)', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'config',
|
||||
type: 'object',
|
||||
value: { key: 'value1' },
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'config', type: 'object', value: { key: 'value1' } },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'config',
|
||||
type: 'object',
|
||||
value: { key: 'value2' },
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'config', type: 'object', value: { key: 'value2' } },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
@@ -2357,22 +2328,16 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should handle complex variable values (arrays)', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
value: [1, 2, 3],
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'items', type: 'array', value: [1, 2, 3] },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap({
|
||||
id: 'var1',
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
value: [1, 2, 4],
|
||||
}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'items', type: 'array', value: [1, 2, 4] },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
@@ -2381,18 +2346,18 @@ describe('hasWorkflowChanged', () => {
|
||||
it.concurrent('should not detect change when variable key order differs', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap(
|
||||
{ id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
{ id: 'var2', name: 'count', type: 'number', value: 42 }
|
||||
),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: createVariablesMap(
|
||||
{ id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
{ id: 'var1', name: 'myVar', type: 'string', value: 'hello' }
|
||||
),
|
||||
variables: {
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false)
|
||||
@@ -2875,135 +2840,175 @@ describe('hasWorkflowChanged', () => {
|
||||
describe('Variables (UI-only fields should not trigger change)', () => {
|
||||
it.concurrent('should not detect change when validationError differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(deployedState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'plain',
|
||||
value: 'test',
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(currentState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'plain',
|
||||
value: 'test',
|
||||
validationError: undefined,
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when validationError has value vs missing', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(deployedState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'number',
|
||||
value: 'invalid',
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(currentState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'number',
|
||||
value: 'invalid',
|
||||
validationError: 'Not a valid number',
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should detect change when variable value differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(deployedState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'plain',
|
||||
value: 'old value',
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(currentState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'plain',
|
||||
value: 'new value',
|
||||
}),
|
||||
})
|
||||
validationError: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should detect change when variable is added', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: {},
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(deployedState as any).variables = {}
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(currentState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'plain',
|
||||
value: 'test',
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should detect change when variable is removed', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(deployedState as any).variables = {
|
||||
var1: {
|
||||
id: 'var1',
|
||||
workflowId: 'workflow1',
|
||||
name: 'myVar',
|
||||
type: 'plain',
|
||||
value: 'test',
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: {},
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(currentState as any).variables = {}
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when empty array vs empty object', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
// Intentional type violation to test robustness with malformed data
|
||||
;(deployedState as unknown as Record<string, unknown>).variables = []
|
||||
;(deployedState as any).variables = []
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: {},
|
||||
blocks: {
|
||||
block1: createBlock('block1'),
|
||||
},
|
||||
})
|
||||
;(currentState as any).variables = {}
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
@@ -3146,7 +3151,7 @@ describe('generateWorkflowDiffSummary', () => {
|
||||
})
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({ id: 'var1', name: 'test', type: 'string', value: 'hello' }),
|
||||
variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'hello' } },
|
||||
})
|
||||
const result = generateWorkflowDiffSummary(currentState, previousState)
|
||||
expect(result.hasChanges).toBe(true)
|
||||
@@ -3156,11 +3161,11 @@ describe('generateWorkflowDiffSummary', () => {
|
||||
it.concurrent('should detect modified variables', () => {
|
||||
const previousState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({ id: 'var1', name: 'test', type: 'string', value: 'hello' }),
|
||||
variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'hello' } },
|
||||
})
|
||||
const currentState = createWorkflowState({
|
||||
blocks: { block1: createBlock('block1') },
|
||||
variables: createVariablesMap({ id: 'var1', name: 'test', type: 'string', value: 'world' }),
|
||||
variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'world' } },
|
||||
})
|
||||
const result = generateWorkflowDiffSummary(currentState, previousState)
|
||||
expect(result.hasChanges).toBe(true)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockSelectChain, createMockUpdateChain } from '@sim/testing'
|
||||
import { loggerMock } from '@sim/testing/mocks'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
@@ -37,7 +35,13 @@ vi.mock('@sim/db/schema', () => ({
|
||||
workflowSchedule: { archivedAt: 'workflow_schedule_archived_at' },
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workflows/utils', () => ({
|
||||
getWorkflowById: (...args: unknown[]) => mockGetWorkflowById(...args),
|
||||
@@ -62,6 +66,24 @@ vi.mock('@/lib/core/telemetry', () => ({
|
||||
|
||||
import { archiveWorkflow } from '@/lib/workflows/lifecycle'
|
||||
|
||||
function createSelectChain<T>(result: T) {
|
||||
const chain = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue(result),
|
||||
}
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
function createUpdateChain() {
|
||||
return {
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
describe('workflow lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -85,10 +107,10 @@ describe('workflow lifecycle', () => {
|
||||
archivedAt: new Date(),
|
||||
})
|
||||
|
||||
mockSelect.mockReturnValue(createMockSelectChain([]))
|
||||
mockSelect.mockReturnValue(createSelectChain([]))
|
||||
|
||||
const tx = {
|
||||
update: vi.fn().mockImplementation(() => createMockUpdateChain()),
|
||||
update: vi.fn().mockImplementation(() => createUpdateChain()),
|
||||
}
|
||||
mockTransaction.mockImplementation(async (callback: (trx: typeof tx) => Promise<void>) =>
|
||||
callback(tx)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
|
||||
import {
|
||||
type ExportWorkflowState,
|
||||
sanitizeForExport,
|
||||
@@ -44,6 +43,36 @@ export interface WorkspaceExportStructure {
|
||||
folders: FolderExportData[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a string for use as a path segment in a ZIP file.
|
||||
*/
|
||||
export function sanitizePathSegment(name: string): string {
|
||||
return name.replace(/[^a-z0-9-_]/gi, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file to the user's device.
|
||||
*/
|
||||
export function downloadFile(
|
||||
content: Blob | string,
|
||||
filename: string,
|
||||
mimeType = 'application/json'
|
||||
): void {
|
||||
try {
|
||||
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
logger.error('Failed to download file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a workflow's state and variables for export.
|
||||
* Returns null if the workflow cannot be fetched.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockDeleteChain, createMockSelectChain, createMockUpdateChain } from '@sim/testing'
|
||||
import { loggerMock } from '@sim/testing/mocks'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockSelect, mockTransaction, mockArchiveWorkflowsForWorkspace, mockGetWorkspaceWithOwner } =
|
||||
@@ -35,7 +33,13 @@ vi.mock('@sim/db/schema', () => ({
|
||||
workspaceNotificationSubscription: { active: 'workspace_notification_active' },
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workflows/lifecycle', () => ({
|
||||
archiveWorkflowsForWorkspace: (...args: unknown[]) => mockArchiveWorkflowsForWorkspace(...args),
|
||||
@@ -47,6 +51,14 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
||||
|
||||
import { archiveWorkspace } from './lifecycle'
|
||||
|
||||
function createUpdateChain() {
|
||||
return {
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
describe('workspace lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -60,12 +72,22 @@ describe('workspace lifecycle', () => {
|
||||
archivedAt: null,
|
||||
})
|
||||
mockArchiveWorkflowsForWorkspace.mockResolvedValue(2)
|
||||
mockSelect.mockReturnValue(createMockSelectChain([{ id: 'server-1' }]))
|
||||
mockSelect.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([{ id: 'server-1' }]),
|
||||
}),
|
||||
})
|
||||
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue(createMockSelectChain([{ id: 'kb-1' }])),
|
||||
update: vi.fn().mockImplementation(() => createMockUpdateChain()),
|
||||
delete: vi.fn().mockImplementation(() => createMockDeleteChain()),
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([{ id: 'kb-1' }]),
|
||||
}),
|
||||
}),
|
||||
update: vi.fn().mockImplementation(() => createUpdateChain()),
|
||||
delete: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}
|
||||
mockTransaction.mockImplementation(async (callback: (trx: typeof tx) => Promise<void>) =>
|
||||
callback(tx)
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@a2a-js/sdk": "0.3.7",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
||||
"@aws-sdk/client-cloudformation": "3.1019.0",
|
||||
"@aws-sdk/client-cloudwatch": "3.940.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "3.940.0",
|
||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||
|
||||
@@ -114,12 +114,6 @@ export const useTableUndoStore = create<TableUndoState>()(
|
||||
if (action.type === 'create-row' && action.rowId === oldRowId) {
|
||||
return { ...entry, action: { ...action, rowId: newRowId } }
|
||||
}
|
||||
if (action.type === 'create-rows') {
|
||||
const patchedRows = action.rows.map((r) =>
|
||||
r.rowId === oldRowId ? { ...r, rowId: newRowId } : r
|
||||
)
|
||||
return { ...entry, action: { ...action, rows: patchedRows } }
|
||||
}
|
||||
return entry
|
||||
})
|
||||
|
||||
|
||||
@@ -32,14 +32,6 @@ export type TableUndoAction =
|
||||
}
|
||||
| { type: 'delete-rows'; rows: DeletedRowSnapshot[] }
|
||||
| { type: 'create-column'; columnName: string; position: number }
|
||||
| {
|
||||
type: 'delete-column'
|
||||
columnName: string
|
||||
columnType: string
|
||||
position: number
|
||||
unique: boolean
|
||||
required: boolean
|
||||
}
|
||||
| { type: 'rename-column'; oldName: string; newName: string }
|
||||
| { type: 'update-column-type'; columnName: string; previousType: string; newType: string }
|
||||
| {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import type {
|
||||
CloudFormationDescribeStackDriftDetectionStatusParams,
|
||||
CloudFormationDescribeStackDriftDetectionStatusResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const describeStackDriftDetectionStatusTool: ToolConfig<
|
||||
CloudFormationDescribeStackDriftDetectionStatusParams,
|
||||
CloudFormationDescribeStackDriftDetectionStatusResponse
|
||||
> = {
|
||||
id: 'cloudformation_describe_stack_drift_detection_status',
|
||||
name: 'CloudFormation Describe Stack Drift Detection Status',
|
||||
description: 'Check the status of a stack drift detection operation',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
stackDriftDetectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The drift detection ID returned by Detect Stack Drift',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/describe-stack-drift-detection-status',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
stackDriftDetectionId: params.stackDriftDetectionId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to describe stack drift detection status')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
stackId: data.output.stackId,
|
||||
stackDriftDetectionId: data.output.stackDriftDetectionId,
|
||||
stackDriftStatus: data.output.stackDriftStatus,
|
||||
detectionStatus: data.output.detectionStatus,
|
||||
detectionStatusReason: data.output.detectionStatusReason,
|
||||
driftedStackResourceCount: data.output.driftedStackResourceCount,
|
||||
timestamp: data.output.timestamp,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
stackId: { type: 'string', description: 'The stack ID' },
|
||||
stackDriftDetectionId: { type: 'string', description: 'The drift detection ID' },
|
||||
stackDriftStatus: {
|
||||
type: 'string',
|
||||
description: 'Drift status (DRIFTED, IN_SYNC, NOT_CHECKED)',
|
||||
},
|
||||
detectionStatus: {
|
||||
type: 'string',
|
||||
description: 'Detection status (DETECTION_IN_PROGRESS, DETECTION_COMPLETE, DETECTION_FAILED)',
|
||||
},
|
||||
detectionStatusReason: { type: 'string', description: 'Reason if detection failed' },
|
||||
driftedStackResourceCount: {
|
||||
type: 'number',
|
||||
description: 'Number of resources that have drifted',
|
||||
},
|
||||
timestamp: { type: 'number', description: 'Timestamp of the detection' },
|
||||
},
|
||||
}
|
||||
85
apps/sim/tools/cloudformation/describe_stack_events.ts
Normal file
85
apps/sim/tools/cloudformation/describe_stack_events.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
CloudFormationDescribeStackEventsParams,
|
||||
CloudFormationDescribeStackEventsResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const describeStackEventsTool: ToolConfig<
|
||||
CloudFormationDescribeStackEventsParams,
|
||||
CloudFormationDescribeStackEventsResponse
|
||||
> = {
|
||||
id: 'cloudformation_describe_stack_events',
|
||||
name: 'CloudFormation Describe Stack Events',
|
||||
description: 'Get the event history for a CloudFormation stack',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
stackName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stack name or ID',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of events to return (default: 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/describe-stack-events',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
stackName: params.stackName,
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to describe CloudFormation stack events')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
events: data.output.events,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'List of stack events with resource status and timestamps',
|
||||
},
|
||||
},
|
||||
}
|
||||
78
apps/sim/tools/cloudformation/describe_stacks.ts
Normal file
78
apps/sim/tools/cloudformation/describe_stacks.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
CloudFormationDescribeStacksParams,
|
||||
CloudFormationDescribeStacksResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const describeStacksTool: ToolConfig<
|
||||
CloudFormationDescribeStacksParams,
|
||||
CloudFormationDescribeStacksResponse
|
||||
> = {
|
||||
id: 'cloudformation_describe_stacks',
|
||||
name: 'CloudFormation Describe Stacks',
|
||||
description: 'List and describe CloudFormation stacks',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
stackName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stack name or ID to describe (omit to list all stacks)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/describe-stacks',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
...(params.stackName && { stackName: params.stackName }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to describe CloudFormation stacks')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
stacks: data.output.stacks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
stacks: {
|
||||
type: 'array',
|
||||
description: 'List of CloudFormation stacks with status, outputs, and tags',
|
||||
},
|
||||
},
|
||||
}
|
||||
78
apps/sim/tools/cloudformation/detect_stack_drift.ts
Normal file
78
apps/sim/tools/cloudformation/detect_stack_drift.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
CloudFormationDetectStackDriftParams,
|
||||
CloudFormationDetectStackDriftResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const detectStackDriftTool: ToolConfig<
|
||||
CloudFormationDetectStackDriftParams,
|
||||
CloudFormationDetectStackDriftResponse
|
||||
> = {
|
||||
id: 'cloudformation_detect_stack_drift',
|
||||
name: 'CloudFormation Detect Stack Drift',
|
||||
description: 'Initiate drift detection on a CloudFormation stack',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
stackName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stack name or ID to detect drift on',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/detect-stack-drift',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
stackName: params.stackName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to detect CloudFormation stack drift')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
stackDriftDetectionId: data.output.stackDriftDetectionId,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
stackDriftDetectionId: {
|
||||
type: 'string',
|
||||
description: 'ID to use with Describe Stack Drift Detection Status to check results',
|
||||
},
|
||||
},
|
||||
}
|
||||
77
apps/sim/tools/cloudformation/get_template.ts
Normal file
77
apps/sim/tools/cloudformation/get_template.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type {
|
||||
CloudFormationGetTemplateParams,
|
||||
CloudFormationGetTemplateResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getTemplateTool: ToolConfig<
|
||||
CloudFormationGetTemplateParams,
|
||||
CloudFormationGetTemplateResponse
|
||||
> = {
|
||||
id: 'cloudformation_get_template',
|
||||
name: 'CloudFormation Get Template',
|
||||
description: 'Retrieve the template body for a CloudFormation stack',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
stackName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stack name or ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/get-template',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
stackName: params.stackName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to get CloudFormation template')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
templateBody: data.output.templateBody,
|
||||
stagesAvailable: data.output.stagesAvailable,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
templateBody: { type: 'string', description: 'The template body as a JSON or YAML string' },
|
||||
stagesAvailable: { type: 'array', description: 'Available template stages' },
|
||||
},
|
||||
}
|
||||
16
apps/sim/tools/cloudformation/index.ts
Normal file
16
apps/sim/tools/cloudformation/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describeStackDriftDetectionStatusTool } from '@/tools/cloudformation/describe_stack_drift_detection_status'
|
||||
import { describeStackEventsTool } from '@/tools/cloudformation/describe_stack_events'
|
||||
import { describeStacksTool } from '@/tools/cloudformation/describe_stacks'
|
||||
import { detectStackDriftTool } from '@/tools/cloudformation/detect_stack_drift'
|
||||
import { getTemplateTool } from '@/tools/cloudformation/get_template'
|
||||
import { listStackResourcesTool } from '@/tools/cloudformation/list_stack_resources'
|
||||
import { validateTemplateTool } from '@/tools/cloudformation/validate_template'
|
||||
|
||||
export const cloudformationDescribeStacksTool = describeStacksTool
|
||||
export const cloudformationListStackResourcesTool = listStackResourcesTool
|
||||
export const cloudformationDetectStackDriftTool = detectStackDriftTool
|
||||
export const cloudformationDescribeStackDriftDetectionStatusTool =
|
||||
describeStackDriftDetectionStatusTool
|
||||
export const cloudformationDescribeStackEventsTool = describeStackEventsTool
|
||||
export const cloudformationGetTemplateTool = getTemplateTool
|
||||
export const cloudformationValidateTemplateTool = validateTemplateTool
|
||||
78
apps/sim/tools/cloudformation/list_stack_resources.ts
Normal file
78
apps/sim/tools/cloudformation/list_stack_resources.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
CloudFormationListStackResourcesParams,
|
||||
CloudFormationListStackResourcesResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listStackResourcesTool: ToolConfig<
|
||||
CloudFormationListStackResourcesParams,
|
||||
CloudFormationListStackResourcesResponse
|
||||
> = {
|
||||
id: 'cloudformation_list_stack_resources',
|
||||
name: 'CloudFormation List Stack Resources',
|
||||
description: 'List all resources in a CloudFormation stack',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
stackName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stack name or ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/list-stack-resources',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
stackName: params.stackName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to list CloudFormation stack resources')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
resources: data.output.resources,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'List of stack resources with type, status, and drift information',
|
||||
},
|
||||
},
|
||||
}
|
||||
131
apps/sim/tools/cloudformation/types.ts
Normal file
131
apps/sim/tools/cloudformation/types.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface CloudFormationConnectionConfig {
|
||||
awsRegion: string
|
||||
awsAccessKeyId: string
|
||||
awsSecretAccessKey: string
|
||||
}
|
||||
|
||||
export interface CloudFormationDescribeStacksParams extends CloudFormationConnectionConfig {
|
||||
stackName?: string
|
||||
}
|
||||
|
||||
export interface CloudFormationListStackResourcesParams extends CloudFormationConnectionConfig {
|
||||
stackName: string
|
||||
}
|
||||
|
||||
export interface CloudFormationDetectStackDriftParams extends CloudFormationConnectionConfig {
|
||||
stackName: string
|
||||
}
|
||||
|
||||
export interface CloudFormationDescribeStackDriftDetectionStatusParams
|
||||
extends CloudFormationConnectionConfig {
|
||||
stackDriftDetectionId: string
|
||||
}
|
||||
|
||||
export interface CloudFormationDescribeStackEventsParams extends CloudFormationConnectionConfig {
|
||||
stackName: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudFormationGetTemplateParams extends CloudFormationConnectionConfig {
|
||||
stackName: string
|
||||
}
|
||||
|
||||
export interface CloudFormationValidateTemplateParams extends CloudFormationConnectionConfig {
|
||||
templateBody: string
|
||||
}
|
||||
|
||||
export interface CloudFormationDescribeStacksResponse extends ToolResponse {
|
||||
output: {
|
||||
stacks: {
|
||||
stackName: string
|
||||
stackId: string
|
||||
stackStatus: string
|
||||
stackStatusReason: string | undefined
|
||||
creationTime: number | undefined
|
||||
lastUpdatedTime: number | undefined
|
||||
description: string | undefined
|
||||
enableTerminationProtection: boolean | undefined
|
||||
driftInformation: {
|
||||
stackDriftStatus: string | undefined
|
||||
lastCheckTimestamp: number | undefined
|
||||
} | null
|
||||
outputs: { outputKey: string; outputValue: string; description: string | undefined }[]
|
||||
tags: { key: string; value: string }[]
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudFormationListStackResourcesResponse extends ToolResponse {
|
||||
output: {
|
||||
resources: {
|
||||
logicalResourceId: string
|
||||
physicalResourceId: string | undefined
|
||||
resourceType: string
|
||||
resourceStatus: string
|
||||
resourceStatusReason: string | undefined
|
||||
lastUpdatedTimestamp: number | undefined
|
||||
driftInformation: {
|
||||
stackResourceDriftStatus: string | undefined
|
||||
lastCheckTimestamp: number | undefined
|
||||
} | null
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudFormationDetectStackDriftResponse extends ToolResponse {
|
||||
output: {
|
||||
stackDriftDetectionId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudFormationDescribeStackDriftDetectionStatusResponse extends ToolResponse {
|
||||
output: {
|
||||
stackId: string
|
||||
stackDriftDetectionId: string
|
||||
stackDriftStatus: string | undefined
|
||||
detectionStatus: string
|
||||
detectionStatusReason: string | undefined
|
||||
driftedStackResourceCount: number | undefined
|
||||
timestamp: number | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudFormationDescribeStackEventsResponse extends ToolResponse {
|
||||
output: {
|
||||
events: {
|
||||
stackId: string
|
||||
eventId: string
|
||||
stackName: string
|
||||
logicalResourceId: string | undefined
|
||||
physicalResourceId: string | undefined
|
||||
resourceType: string | undefined
|
||||
resourceStatus: string | undefined
|
||||
resourceStatusReason: string | undefined
|
||||
timestamp: number | undefined
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudFormationGetTemplateResponse extends ToolResponse {
|
||||
output: {
|
||||
templateBody: string
|
||||
stagesAvailable: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudFormationValidateTemplateResponse extends ToolResponse {
|
||||
output: {
|
||||
description: string | undefined
|
||||
parameters: {
|
||||
parameterKey: string | undefined
|
||||
defaultValue: string | undefined
|
||||
noEcho: boolean | undefined
|
||||
description: string | undefined
|
||||
}[]
|
||||
capabilities: string[]
|
||||
capabilitiesReason: string | undefined
|
||||
declaredTransforms: string[]
|
||||
}
|
||||
}
|
||||
89
apps/sim/tools/cloudformation/validate_template.ts
Normal file
89
apps/sim/tools/cloudformation/validate_template.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
CloudFormationValidateTemplateParams,
|
||||
CloudFormationValidateTemplateResponse,
|
||||
} from '@/tools/cloudformation/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const validateTemplateTool: ToolConfig<
|
||||
CloudFormationValidateTemplateParams,
|
||||
CloudFormationValidateTemplateResponse
|
||||
> = {
|
||||
id: 'cloudformation_validate_template',
|
||||
name: 'CloudFormation Validate Template',
|
||||
description: 'Validate a CloudFormation template for syntax and structural correctness',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
templateBody: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The CloudFormation template body (JSON or YAML)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudformation/validate-template',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
templateBody: params.templateBody,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to validate CloudFormation template')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
description: data.output.description,
|
||||
parameters: data.output.parameters,
|
||||
capabilities: data.output.capabilities,
|
||||
capabilitiesReason: data.output.capabilitiesReason,
|
||||
declaredTransforms: data.output.declaredTransforms,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
description: { type: 'string', description: 'Template description' },
|
||||
parameters: {
|
||||
type: 'array',
|
||||
description: 'Template parameters with defaults and descriptions',
|
||||
},
|
||||
capabilities: { type: 'array', description: 'Required capabilities (e.g., CAPABILITY_IAM)' },
|
||||
capabilitiesReason: { type: 'string', description: 'Reason capabilities are required' },
|
||||
declaredTransforms: {
|
||||
type: 'array',
|
||||
description: 'Transforms used in the template (e.g., AWS::Serverless-2016-10-31)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -275,6 +275,15 @@ import {
|
||||
cloudflareUpdateDnsRecordTool,
|
||||
cloudflareUpdateZoneSettingTool,
|
||||
} from '@/tools/cloudflare'
|
||||
import {
|
||||
cloudformationDescribeStackDriftDetectionStatusTool,
|
||||
cloudformationDescribeStackEventsTool,
|
||||
cloudformationDescribeStacksTool,
|
||||
cloudformationDetectStackDriftTool,
|
||||
cloudformationGetTemplateTool,
|
||||
cloudformationListStackResourcesTool,
|
||||
cloudformationValidateTemplateTool,
|
||||
} from '@/tools/cloudformation'
|
||||
import {
|
||||
cloudwatchDescribeAlarmsTool,
|
||||
cloudwatchDescribeLogGroupsTool,
|
||||
@@ -3385,6 +3394,14 @@ export const tools: Record<string, ToolConfig> = {
|
||||
rds_delete: rdsDeleteTool,
|
||||
rds_execute: rdsExecuteTool,
|
||||
rds_introspect: rdsIntrospectTool,
|
||||
cloudformation_describe_stacks: cloudformationDescribeStacksTool,
|
||||
cloudformation_list_stack_resources: cloudformationListStackResourcesTool,
|
||||
cloudformation_detect_stack_drift: cloudformationDetectStackDriftTool,
|
||||
cloudformation_describe_stack_drift_detection_status:
|
||||
cloudformationDescribeStackDriftDetectionStatusTool,
|
||||
cloudformation_describe_stack_events: cloudformationDescribeStackEventsTool,
|
||||
cloudformation_get_template: cloudformationGetTemplateTool,
|
||||
cloudformation_validate_template: cloudformationValidateTemplateTool,
|
||||
cloudwatch_query_logs: cloudwatchQueryLogsTool,
|
||||
cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool,
|
||||
cloudwatch_describe_alarms: cloudwatchDescribeAlarmsTool,
|
||||
|
||||
75
bun.lock
75
bun.lock
@@ -57,6 +57,7 @@
|
||||
"@a2a-js/sdk": "0.3.7",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
||||
"@aws-sdk/client-cloudformation": "3.1019.0",
|
||||
"@aws-sdk/client-cloudwatch": "3.940.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "3.940.0",
|
||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||
@@ -416,6 +417,8 @@
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/eventstream-handler-node": "3.936.0", "@aws-sdk/middleware-eventstream": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/middleware-websocket": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/token-providers": "3.940.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Gs6UUQP1zt8vahOxJ3BADcb3B+2KldUNA3bKa+KdK58de7N7tLJFJfZuXhFGGtwyNPh1aw6phtdP6dauq3OLWA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation": ["@aws-sdk/client-cloudformation@3.1019.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-RNBtkQQ5IUqTdxaAe7ADwlJ/1qqW5kONLD1Mxr7PUWteEQwYR9ZJYscDul2qNkCWhu/vMKhk+qwJKPkdu2TNzA=="],
|
||||
|
||||
"@aws-sdk/client-cloudwatch": ["@aws-sdk/client-cloudwatch@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-compression": "^4.3.12", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-C35xpPntRAGdEg3X5iKpSUCBaP3yxYNo1U95qipN/X1e0/TYIDWHwGt8Z1ntRafK19jp5oVzhRQ+PD1JAPSEzA=="],
|
||||
|
||||
"@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7dEIO3D98IxA9IhqixPJbzQsBkk4TchHHpFdd0JOhlSlihWhiwbf3ijUePJVXYJxcpRRtMmAMtDRLDzCSO+ZHg=="],
|
||||
@@ -3852,6 +3855,46 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/core": ["@aws-sdk/core@3.973.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.29", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.14", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.28", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-serde": "^4.2.16", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.46", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.16", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/smithy-client": ["@smithy/smithy-client@4.12.8", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.44", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.48", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/util-retry": ["@smithy/util-retry@4.2.13", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ=="],
|
||||
|
||||
"@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.24", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw=="],
|
||||
|
||||
"@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.25", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.22", "@aws-sdk/credential-provider-http": "^3.972.24", "@aws-sdk/credential-provider-ini": "^3.972.24", "@aws-sdk/credential-provider-process": "^3.972.22", "@aws-sdk/credential-provider-sso": "^3.972.24", "@aws-sdk/credential-provider-web-identity": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg=="],
|
||||
@@ -4534,6 +4577,24 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-login": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/token-providers": "3.1021.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="],
|
||||
|
||||
"@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
|
||||
|
||||
"@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.24", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg=="],
|
||||
@@ -5092,6 +5153,20 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1021.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA=="],
|
||||
|
||||
"@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="],
|
||||
|
||||
"@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
"@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.24", "@aws-sdk/nested-clients": "^3.996.14", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g=="],
|
||||
|
||||
@@ -118,15 +118,6 @@ export {
|
||||
type SerializedConnection,
|
||||
type SerializedWorkflow,
|
||||
} from './serialized-block.factory'
|
||||
export {
|
||||
createTableColumn,
|
||||
createTableRow,
|
||||
type TableColumnFactoryOptions,
|
||||
type TableColumnFixture,
|
||||
type TableColumnType,
|
||||
type TableRowFactoryOptions,
|
||||
type TableRowFixture,
|
||||
} from './table.factory'
|
||||
// Tool mock responses
|
||||
export {
|
||||
mockDriveResponses,
|
||||
@@ -187,10 +178,3 @@ export {
|
||||
type WorkflowFactoryOptions,
|
||||
type WorkflowStateFixture,
|
||||
} from './workflow.factory'
|
||||
export {
|
||||
createWorkflowVariable,
|
||||
createWorkflowVariablesMap,
|
||||
type WorkflowVariableFactoryOptions,
|
||||
type WorkflowVariableFixture,
|
||||
type WorkflowVariableType,
|
||||
} from './workflow-variable.factory'
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createTableColumn } from './table.factory'
|
||||
|
||||
describe('table factory', () => {
|
||||
it('generates default column names that match table naming rules', () => {
|
||||
const generatedNames = Array.from({ length: 100 }, () => createTableColumn().name)
|
||||
|
||||
for (const name of generatedNames) {
|
||||
expect(name).toMatch(/^[a-z_][a-z0-9_]*$/)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { customAlphabet, nanoid } from 'nanoid'
|
||||
|
||||
export type TableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'json'
|
||||
|
||||
export interface TableColumnFixture {
|
||||
name: string
|
||||
type: TableColumnType
|
||||
required?: boolean
|
||||
unique?: boolean
|
||||
}
|
||||
|
||||
export interface TableRowFixture {
|
||||
id: string
|
||||
data: Record<string, unknown>
|
||||
position: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface TableColumnFactoryOptions {
|
||||
name?: string
|
||||
type?: TableColumnType
|
||||
required?: boolean
|
||||
unique?: boolean
|
||||
}
|
||||
|
||||
export interface TableRowFactoryOptions {
|
||||
id?: string
|
||||
data?: Record<string, unknown>
|
||||
position?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
const createTableColumnSuffix = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789_', 6)
|
||||
|
||||
/**
|
||||
* Creates a table column fixture with sensible defaults.
|
||||
*/
|
||||
export function createTableColumn(options: TableColumnFactoryOptions = {}): TableColumnFixture {
|
||||
return {
|
||||
name: options.name ?? `column_${createTableColumnSuffix()}`,
|
||||
type: options.type ?? 'string',
|
||||
required: options.required,
|
||||
unique: options.unique,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table row fixture with sensible defaults.
|
||||
*/
|
||||
export function createTableRow(options: TableRowFactoryOptions = {}): TableRowFixture {
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
return {
|
||||
id: options.id ?? `row_${nanoid(8)}`,
|
||||
data: options.data ?? {},
|
||||
position: options.position ?? 0,
|
||||
createdAt: options.createdAt ?? timestamp,
|
||||
updatedAt: options.updatedAt ?? timestamp,
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export type WorkflowVariableType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain'
|
||||
|
||||
export interface WorkflowVariableFixture {
|
||||
id: string
|
||||
name: string
|
||||
type: WorkflowVariableType
|
||||
value: unknown
|
||||
workflowId?: string
|
||||
validationError?: string
|
||||
}
|
||||
|
||||
export interface WorkflowVariableFactoryOptions {
|
||||
id?: string
|
||||
name?: string
|
||||
type?: WorkflowVariableType
|
||||
value?: unknown
|
||||
workflowId?: string
|
||||
validationError?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow variable fixture with sensible defaults.
|
||||
*/
|
||||
export function createWorkflowVariable(
|
||||
options: WorkflowVariableFactoryOptions = {}
|
||||
): WorkflowVariableFixture {
|
||||
const id = options.id ?? `var_${nanoid(8)}`
|
||||
|
||||
return {
|
||||
id,
|
||||
name: options.name ?? `variable_${id.slice(0, 4)}`,
|
||||
type: options.type ?? 'string',
|
||||
value: options.value ?? '',
|
||||
workflowId: options.workflowId,
|
||||
validationError: options.validationError,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a variables map keyed by variable id.
|
||||
*/
|
||||
export function createWorkflowVariablesMap(
|
||||
variables: WorkflowVariableFactoryOptions[] = []
|
||||
): Record<string, WorkflowVariableFixture> {
|
||||
return Object.fromEntries(
|
||||
variables.map((variable) => {
|
||||
const fixture = createWorkflowVariable(variable)
|
||||
return [fixture.id, fixture]
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -46,14 +46,10 @@ export * from './builders'
|
||||
export * from './factories'
|
||||
export {
|
||||
AuthTypeMock,
|
||||
asyncRouteParams,
|
||||
auditMock,
|
||||
clearRedisMocks,
|
||||
createEditWorkflowRegistryMock,
|
||||
createEnvMock,
|
||||
createFeatureFlagsMock,
|
||||
createMockDb,
|
||||
createMockDeleteChain,
|
||||
createMockFetch,
|
||||
createMockFormDataRequest,
|
||||
createMockGetEnv,
|
||||
@@ -61,19 +57,15 @@ export {
|
||||
createMockRedis,
|
||||
createMockRequest,
|
||||
createMockResponse,
|
||||
createMockSelectChain,
|
||||
createMockSocket,
|
||||
createMockStorage,
|
||||
createMockUpdateChain,
|
||||
databaseMock,
|
||||
defaultMockEnv,
|
||||
defaultMockUser,
|
||||
drizzleOrmMock,
|
||||
envMock,
|
||||
featureFlagsMock,
|
||||
loggerMock,
|
||||
type MockAuthResult,
|
||||
type MockFeatureFlags,
|
||||
type MockFetchResponse,
|
||||
type MockHybridAuthResult,
|
||||
type MockRedis,
|
||||
|
||||
@@ -103,38 +103,6 @@ export function createMockDb() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a select chain that resolves from `where()`.
|
||||
*/
|
||||
export function createMockSelectChain<T>(result: T) {
|
||||
return {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
leftJoin: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue(result),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an update chain that resolves from `where()`.
|
||||
*/
|
||||
export function createMockUpdateChain<T>(result: T = [] as T) {
|
||||
return {
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue(result),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a delete chain that resolves from `where()`.
|
||||
*/
|
||||
export function createMockDeleteChain<T>(result: T = [] as T) {
|
||||
return {
|
||||
where: vi.fn().mockResolvedValue(result),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock module for @sim/db.
|
||||
* Use with vi.mock() to replace the real database.
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
const editWorkflowBlockConfigs: Record<
|
||||
string,
|
||||
{
|
||||
type: string
|
||||
name: string
|
||||
outputs: Record<string, unknown>
|
||||
subBlocks: { id: string; type: string }[]
|
||||
}
|
||||
> = {
|
||||
condition: {
|
||||
type: 'condition',
|
||||
name: 'Condition',
|
||||
outputs: {},
|
||||
subBlocks: [{ id: 'conditions', type: 'condition-input' }],
|
||||
},
|
||||
agent: {
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Default content output' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'systemPrompt', type: 'long-input' },
|
||||
{ id: 'model', type: 'combobox' },
|
||||
{ id: 'responseFormat', type: 'response-format' },
|
||||
],
|
||||
},
|
||||
function: {
|
||||
type: 'function',
|
||||
name: 'Function',
|
||||
outputs: {},
|
||||
subBlocks: [
|
||||
{ id: 'code', type: 'code' },
|
||||
{ id: 'language', type: 'dropdown' },
|
||||
],
|
||||
},
|
||||
router_v2: {
|
||||
type: 'router_v2',
|
||||
name: 'Router',
|
||||
outputs: {},
|
||||
subBlocks: [{ id: 'routes', type: 'router-input' }],
|
||||
},
|
||||
}
|
||||
|
||||
export function createEditWorkflowRegistryMock(types?: string[]) {
|
||||
const enabledTypes = new Set(types ?? Object.keys(editWorkflowBlockConfigs))
|
||||
const blocks = Object.fromEntries(
|
||||
Object.entries(editWorkflowBlockConfigs).filter(([type]) => enabledTypes.has(type))
|
||||
)
|
||||
|
||||
return {
|
||||
getAllBlocks: () => Object.values(blocks),
|
||||
getBlock: (type: string) => blocks[type],
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
export interface MockFeatureFlags {
|
||||
isProd: boolean
|
||||
isDev: boolean
|
||||
isTest: boolean
|
||||
isHosted: boolean
|
||||
isBillingEnabled: boolean
|
||||
isEmailVerificationEnabled: boolean
|
||||
isAuthDisabled: boolean
|
||||
isRegistrationDisabled: boolean
|
||||
isEmailPasswordEnabled: boolean
|
||||
isSignupEmailValidationEnabled: boolean
|
||||
isTriggerDevEnabled: boolean
|
||||
isSsoEnabled: boolean
|
||||
isCredentialSetsEnabled: boolean
|
||||
isAccessControlEnabled: boolean
|
||||
isOrganizationsEnabled: boolean
|
||||
isInboxEnabled: boolean
|
||||
isE2bEnabled: boolean
|
||||
isAzureConfigured: boolean
|
||||
isInvitationsDisabled: boolean
|
||||
isPublicApiDisabled: boolean
|
||||
isReactGrabEnabled: boolean
|
||||
isReactScanEnabled: boolean
|
||||
getAllowedIntegrationsFromEnv: () => string[] | null
|
||||
getAllowedMcpDomainsFromEnv: () => string[] | null
|
||||
getCostMultiplier: () => number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mutable mock for the feature flags module.
|
||||
*/
|
||||
export function createFeatureFlagsMock(
|
||||
overrides: Partial<MockFeatureFlags> = {}
|
||||
): MockFeatureFlags {
|
||||
return {
|
||||
isProd: false,
|
||||
isDev: false,
|
||||
isTest: true,
|
||||
isHosted: false,
|
||||
isBillingEnabled: false,
|
||||
isEmailVerificationEnabled: false,
|
||||
isAuthDisabled: false,
|
||||
isRegistrationDisabled: false,
|
||||
isEmailPasswordEnabled: true,
|
||||
isSignupEmailValidationEnabled: false,
|
||||
isTriggerDevEnabled: false,
|
||||
isSsoEnabled: false,
|
||||
isCredentialSetsEnabled: false,
|
||||
isAccessControlEnabled: false,
|
||||
isOrganizationsEnabled: false,
|
||||
isInboxEnabled: false,
|
||||
isE2bEnabled: false,
|
||||
isAzureConfigured: false,
|
||||
isInvitationsDisabled: false,
|
||||
isPublicApiDisabled: false,
|
||||
isReactGrabEnabled: false,
|
||||
isReactScanEnabled: false,
|
||||
getAllowedIntegrationsFromEnv: () => null,
|
||||
getAllowedMcpDomainsFromEnv: () => null,
|
||||
getCostMultiplier: () => 1,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
export const featureFlagsMock = createFeatureFlagsMock()
|
||||
@@ -16,6 +16,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
// API mocks
|
||||
export {
|
||||
mockCommonSchemas,
|
||||
mockConsoleLogger,
|
||||
@@ -23,13 +24,16 @@ export {
|
||||
mockKnowledgeSchemas,
|
||||
setupCommonApiMocks,
|
||||
} from './api.mock'
|
||||
// Audit mocks
|
||||
export { auditMock } from './audit.mock'
|
||||
// Auth mocks
|
||||
export {
|
||||
defaultMockUser,
|
||||
type MockAuthResult,
|
||||
type MockUser,
|
||||
mockAuth,
|
||||
} from './auth.mock'
|
||||
// Blocks mocks
|
||||
export {
|
||||
blocksMock,
|
||||
createMockGetBlock,
|
||||
@@ -38,23 +42,18 @@ export {
|
||||
mockToolConfigs,
|
||||
toolsUtilsMock,
|
||||
} from './blocks.mock'
|
||||
// Database mocks
|
||||
export {
|
||||
createMockDb,
|
||||
createMockDeleteChain,
|
||||
createMockSelectChain,
|
||||
createMockSql,
|
||||
createMockSqlOperators,
|
||||
createMockUpdateChain,
|
||||
databaseMock,
|
||||
drizzleOrmMock,
|
||||
} from './database.mock'
|
||||
export { createEditWorkflowRegistryMock } from './edit-workflow.mock'
|
||||
// Env mocks
|
||||
export { createEnvMock, createMockGetEnv, defaultMockEnv, envMock } from './env.mock'
|
||||
export {
|
||||
createFeatureFlagsMock,
|
||||
featureFlagsMock,
|
||||
type MockFeatureFlags,
|
||||
} from './feature-flags.mock'
|
||||
// Executor mocks - use side-effect import: import '@sim/testing/mocks/executor'
|
||||
// Fetch mocks
|
||||
export {
|
||||
createMockFetch,
|
||||
createMockResponse,
|
||||
@@ -64,21 +63,24 @@ export {
|
||||
mockNextFetchResponse,
|
||||
setupGlobalFetchMock,
|
||||
} from './fetch.mock'
|
||||
// Hybrid auth mocks
|
||||
export { AuthTypeMock, type MockHybridAuthResult, mockHybridAuth } from './hybrid-auth.mock'
|
||||
// Logger mocks
|
||||
export { clearLoggerMocks, createMockLogger, getLoggerCalls, loggerMock } from './logger.mock'
|
||||
// Redis mocks
|
||||
export { clearRedisMocks, createMockRedis, type MockRedis } from './redis.mock'
|
||||
export {
|
||||
asyncRouteParams,
|
||||
createMockFormDataRequest,
|
||||
createMockRequest,
|
||||
requestUtilsMock,
|
||||
} from './request.mock'
|
||||
// Request mocks
|
||||
export { createMockFormDataRequest, createMockRequest, requestUtilsMock } from './request.mock'
|
||||
// Socket mocks
|
||||
export {
|
||||
createMockSocket,
|
||||
createMockSocketServer,
|
||||
type MockSocket,
|
||||
type MockSocketServer,
|
||||
} from './socket.mock'
|
||||
// Storage mocks
|
||||
export { clearStorageMocks, createMockStorage, setupGlobalStorageMocks } from './storage.mock'
|
||||
// Telemetry mocks
|
||||
export { telemetryMock } from './telemetry.mock'
|
||||
// UUID mocks
|
||||
export { mockCryptoUuid, mockUuid } from './uuid.mock'
|
||||
|
||||
@@ -59,13 +59,6 @@ export function createMockFormDataRequest(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the async `params` object used by App Router route handlers.
|
||||
*/
|
||||
export function asyncRouteParams<T extends Record<string, unknown>>(params: T): Promise<T> {
|
||||
return Promise.resolve(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured mock for @/lib/core/utils/request module.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user