mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 08:07:15 +00:00
Merge remote-tracking branch 'origin/main' into tl/workspace-to-hub
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## [1.751.0](https://github.com/windmill-labs/windmill/compare/v1.750.0...v1.751.0) (2026-07-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add cosmetic dev/staging label for dev workspaces ([#9959](https://github.com/windmill-labs/windmill/issues/9959)) ([fd8e64d](https://github.com/windmill-labs/windmill/commit/fd8e64d11fea3ffdb7858100c67cb2e9ca841ed6))
|
||||
* **auth:** add runtime NO_AUTH mode for authentication bypass ([#9962](https://github.com/windmill-labs/windmill/issues/9962)) ([91e1b08](https://github.com/windmill-labs/windmill/commit/91e1b087a206efb7189824b4184e1f3f4cda7211))
|
||||
* **frontend:** custom skills — detail modal, batch manage, shared validation ([#9847](https://github.com/windmill-labs/windmill/issues/9847)) ([2e14302](https://github.com/windmill-labs/windmill/commit/2e14302e4abbad595584806bff12548d520fcb58))
|
||||
* **pipelines:** auto-derive cascade edges from ducklake/s3 reads (+ muted-read badge) ([#9963](https://github.com/windmill-labs/windmill/issues/9963)) ([3dcd394](https://github.com/windmill-labs/windmill/commit/3dcd3949a14199b106506994ea31ca3de7e636b3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai:** centralize Anthropic Messages API routing across completion paths ([#9960](https://github.com/windmill-labs/windmill/issues/9960)) ([cc2f638](https://github.com/windmill-labs/windmill/commit/cc2f638de6cebeffb9fee1d4835a0cfd565af86c))
|
||||
* **assets:** responsive layout for small screens ([#9961](https://github.com/windmill-labs/windmill/issues/9961)) ([45946d1](https://github.com/windmill-labs/windmill/commit/45946d1185c0bd07948d4d8454880c2801571f9d))
|
||||
* **cli:** quote non-identifier property names in resource-type namespace ([#9964](https://github.com/windmill-labs/windmill/issues/9964)) ([dc6b997](https://github.com/windmill-labs/windmill/commit/dc6b99775b550e7433fee8a159c30eaf296500c5))
|
||||
* critical alerts modal mute toggles no longer close popover or fail to save ([#9969](https://github.com/windmill-labs/windmill/issues/9969)) ([6587019](https://github.com/windmill-labs/windmill/commit/6587019d263374ee5707d258f5d8eec7e73c690d))
|
||||
* **frontend:** theme-aware code block background in prose markdown ([#9968](https://github.com/windmill-labs/windmill/issues/9968)) ([9821596](https://github.com/windmill-labs/windmill/commit/9821596251cff698958ffbfbd11fffa6a7988c6c))
|
||||
|
||||
## [1.750.0](https://github.com/windmill-labs/windmill/compare/v1.749.0...v1.750.0) (2026-07-06)
|
||||
|
||||
|
||||
|
||||
+18
-17
@@ -1,7 +1,7 @@
|
||||
ARG DEBIAN_IMAGE=debian:bookworm-slim
|
||||
ARG RUST_IMAGE=rust:1.93-slim-bookworm
|
||||
ARG DEBIAN_IMAGE=debian:trixie-slim
|
||||
ARG RUST_IMAGE=rust:1.93-slim-trixie
|
||||
|
||||
FROM debian:bookworm-slim AS nsjail
|
||||
FROM debian:trixie-slim AS nsjail
|
||||
|
||||
WORKDIR /nsjail
|
||||
|
||||
@@ -9,12 +9,12 @@ RUN apt-get -y update \
|
||||
&& apt-get install -y \
|
||||
bison=2:3.8.* \
|
||||
flex=2.6.* \
|
||||
g++=4:12.2.* \
|
||||
gcc=4:12.2.* \
|
||||
git=1:2.39.* \
|
||||
g++=4:14.2.* \
|
||||
gcc=4:14.2.* \
|
||||
git=1:2.47.* \
|
||||
libprotobuf-dev=3.21.* \
|
||||
libnl-route-3-dev=3.7.* \
|
||||
make=4.3-4.1 \
|
||||
make=4.4.* \
|
||||
pkg-config=1.8.* \
|
||||
protobuf-compiler=3.21.*
|
||||
|
||||
@@ -44,7 +44,7 @@ FROM rust_base AS windmill_duckdb_ffi_internal_builder
|
||||
|
||||
WORKDIR /windmill-duckdb-ffi-internal
|
||||
|
||||
RUN apt-get update && apt-get install -y clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \
|
||||
RUN apt-get update && apt-get install -y clang=1:19.0* libclang-dev=1:19.0* cmake=3.31.* && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -98,7 +98,7 @@ ARG features=""
|
||||
|
||||
COPY --from=planner /windmill/recipe.json recipe.json
|
||||
|
||||
RUN apt-get update && apt-get install -y libxml2-dev=2.9.* libxmlsec1-dev=1.2.* libkrb5-dev libsasl2-dev libcurl4-openssl-dev clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \
|
||||
RUN apt-get update && apt-get install -y libxml2-dev=2.12.* libxmlsec1-dev=1.2.* libkrb5-dev libsasl2-dev libcurl4-openssl-dev clang=1:19.0* libclang-dev=1:19.0* cmake=3.31.* && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -135,7 +135,6 @@ FROM ${DEBIAN_IMAGE}
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG POWERSHELL_VERSION=7.5.0
|
||||
ARG POWERSHELL_DEB_VERSION=7.5.0-1
|
||||
ARG KUBECTL_VERSION=1.28.7
|
||||
ARG HELM_VERSION=3.14.3
|
||||
# NOTE: If changing, also change go version in workspace dependencies template at WorkspaceDependenciesEditor.svelte
|
||||
@@ -183,12 +182,14 @@ RUN if [ "$WITH_GIT" = "true" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
else echo 'Building the image without git'; fi;
|
||||
|
||||
# PowerShell ships as a tarball: the upstream .deb depends on libicu<=74 which no longer exists in trixie
|
||||
RUN if [ "$WITH_POWERSHELL" = "true" ]; then \
|
||||
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu72 -y && wget -O 'pwsh.deb' "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell_${POWERSHELL_DEB_VERSION}.deb_amd64.deb" && apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
dpkg --install 'pwsh.deb' && \
|
||||
rm 'pwsh.deb'; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then apt-get update -y && apt install libicu72 -y && wget -O powershell.tar.gz "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-arm64.tar.gz" && apt-get clean \
|
||||
case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") pwsh_arch=x64 ;; \
|
||||
"linux/arm64") pwsh_arch=arm64 ;; \
|
||||
*) pwsh_arch="" ;; \
|
||||
esac; \
|
||||
if [ -n "$pwsh_arch" ]; then apt-get update -y && apt install libicu76 -y && wget -O powershell.tar.gz "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-${pwsh_arch}.tar.gz" && apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
mkdir -p /opt/microsoft/powershell/7 && \
|
||||
tar zxf powershell.tar.gz -C /opt/microsoft/powershell/7 && \
|
||||
@@ -292,7 +293,7 @@ RUN bun install -g windmill-cli \
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash \
|
||||
&& cp /root/.local/share/claude/versions/* /usr/bin/claude
|
||||
|
||||
COPY --from=php:8.3.30-cli-bookworm /usr/local/bin/php /usr/bin/php
|
||||
COPY --from=php:8.3.30-cli-trixie /usr/local/bin/php /usr/bin/php
|
||||
COPY --from=composer:2.9.5 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
@@ -303,7 +304,7 @@ ENV CARGO_HOME="/tmp/windmill/cache/cargo"
|
||||
ENV LD_LIBRARY_PATH="."
|
||||
|
||||
# nsjail runtime deps and binary
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libprotobuf32 libnl-route-3-200 libnl-3-200 \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libprotobuf32t64 libnl-route-3-200 libnl-3-200 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM datatable_migrations WHERE workspace_id = $1 AND datatable = ANY($2::text[])",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00e9fc3fed9379264881c58df9404ab3569a0611b33c261602d62e9c847c583e"
|
||||
}
|
||||
+5
-5
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "15c0584e9eb078442f6928adb894324c47e010e3554c14e755a7e045453745bb"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT ws.datatable->'datatables' AS datatable_name\n FROM workspace_settings ws\n WHERE ws.workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "datatable_name",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "16a67b92dbd32024838983184e6974f2ce577b78decd6b821096c9f2f252ae8b"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE datatable_migrations SET datatable = $3 WHERE workspace_id = $1 AND datatable = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c543583bcf7bcaf2f422638ce0741a2e193f18ffa11d08e8ca49cef5c3b850c"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 AND timestamp = $3 RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3c057e0284f1219865b8f7dc6eb3c1293c4b72d0b52abeefd1e954617984a2d8"
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, code_up, code_down FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 AND timestamp = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "code_up",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "code_down",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "44c4e9848bc97b66a49d5454d30d40dfe1ffe51839884f3e9e9360d9e17b5afd"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace\n (id, name, owner, parent_workspace_id, is_dev_workspace, dev_workspace_label)\n VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "530a797e67ff352471f1b34f260dd530a653081619e6d3132bf07996520b1e25"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 RETURNING timestamp, name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "timestamp",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "564f4bdf43135c603518ef35a3e95bde7f479877018980cffe1c1e9d34aad59e"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 AND timestamp = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5b67c3af6477d7029d118ac259a7535d4d962c328a060f33c07191ac3e033e82"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ws.datatable->'datatables'->$2 FROM workspace_settings ws WHERE ws.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5cca3509374b1ddd8707c930bc382734e4ca6cad2d849f75a8f8a249f83df83f"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace SET dev_workspace_label = $1 WHERE id = $2 AND is_dev_workspace RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "63d6d968905cf82fb3bb0577d41a29a8f88010891fcf3eaf4761a57461f97703"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, code_down FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 AND timestamp = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "code_down",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6ac00d65b3b7707cba9456171d4332c696e12b52066a5ae2ab10fe3b5bd0f3d0"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO datatable_migrations (workspace_id, datatable, timestamp, name, code_up, code_down) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "760908f44cafb500e4ba2515d1b030390d9510e73e9b7df347b697d6dc7aefe6"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT (ws.datatable->'datatables'->$2->>'migrations_enabled')::boolean FROM workspace_settings ws WHERE ws.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "798dc8ce1c80b5ebd8120f55b6c9f909bf4babb29910514753cb822dde4e7ca9"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, name, dev_workspace_label FROM workspace WHERE parent_workspace_id = $1 AND is_dev_workspace AND deleted = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "dev_workspace_label",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "868985685d95197efc534bb2f3e0c956bea94dffb46b3b838c096f04b66d6c52"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace, dev_workspace_label)\n SELECT $1, $2, owner, false, premium,\n CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5,\n CASE WHEN $5 THEN dev_workspace_label ELSE NULL END\n FROM workspace WHERE id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8ed229e88dc49b0ba7328d48f991493fcdbab4ae91cae271ea911f5b14ecf0d4"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT MAX(timestamp) FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "max",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "95e370a82ef46d9310f77a99b437a20a5773113e290d69a016363543067baf14"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET datatable = jsonb_set(datatable, ARRAY['datatables', $2::text, 'migrations_enabled'], 'false'::jsonb) WHERE workspace_id = $1 AND jsonb_exists(datatable->'datatables', $2) RETURNING 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9eea74f68a0c99d5ce6601fd854c1d18df0ab30f528ef4ce7910c43dd44f4cfb"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace SET parent_workspace_id = $1, is_dev_workspace = true, dev_workspace_label = $3 WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9f567f04f67ce3b197eaa641eaf2d0bbe1f5aff27187505778b6abe2c87a5d01"
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT datatable, timestamp, name, code_up, code_down FROM datatable_migrations WHERE workspace_id = $1 ORDER BY datatable, timestamp ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "datatable",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "timestamp",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "code_up",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "code_down",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a8cec061756c7be61af829f4a5207ba6333ad5feb4557a9341427e9e68608025"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO datatable_migrations (workspace_id, datatable, timestamp, name, code_up, code_down)\n SELECT $2, datatable, timestamp, name, code_up, code_down\n FROM datatable_migrations WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "aef927110ef4cff51d3faabb0c389730dd215f4e90d105f0e86025f0ac42f020"
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n workspace.is_dev_workspace, workspace.dev_workspace_label,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "parent_workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_dev_workspace",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "dev_workspace_label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "operator_settings",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "disabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "af19b9e3deb4f5c9e6ba77963a5da5c60aa6878b2c77f6028a68d64f797c3322"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pg_advisory_xact_lock",
|
||||
"type_info": "Void"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "b3200181380df2f645bfce270dc1cf520939fa4fa4b65371d11db7416ba0b14c"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO datatable_migrations (workspace_id, datatable, timestamp, name, code_up, code_down) VALUES ($1, $2, $3, 'initial', $4, NULL)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b96c1a720654ae8ae52e8098cc18d887920beaec97696a17da020a2adfb052f0"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO datatable_migrations (workspace_id, datatable, timestamp, name, code_up, code_down) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (workspace_id, datatable, timestamp) DO UPDATE SET name = EXCLUDED.name, code_up = EXCLUDED.code_up, code_down = EXCLUDED.code_down",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c5639d48c92d6863f16f97de91eae590d0ed2f4b7831956bb429a47c478f7c1f"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ws.datatable->'datatables' FROM workspace_settings ws WHERE ws.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c9d9c08505e69790154876f50f90d503a92e44e291a76a53ef09ded619edfacb"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT datatable, timestamp FROM datatable_migrations WHERE workspace_id = $1 AND datatable = ANY($2) AND timestamp = ANY($3)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "datatable",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "timestamp",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cf2e74dcd0992f22eb3b62995fd0099ee5f46dafba8f323cf002e329ac69d1ac"
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT timestamp, name, code_up, code_down FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 ORDER BY timestamp ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "timestamp",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "code_up",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "code_down",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e5d8d46b9431033fa7572a880ee0e74e14afde3c02000a88e696d7b6adcad0d6"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET datatable = jsonb_set(datatable, ARRAY['datatables', $2::text, 'migrations_enabled'], 'true'::jsonb) WHERE workspace_id = $1 AND jsonb_exists(datatable->'datatables', $2) RETURNING 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ec8e502f9c2926e578e02cd164a7a3e43ef12b97d55353f86a9a368617efccca"
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT datatable, timestamp, name, code_up, code_down FROM datatable_migrations WHERE workspace_id = $1 ORDER BY datatable, timestamp",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "datatable",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "timestamp",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "code_up",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "code_down",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f89b024949f29eb5142744635914b94fd84c1bb64e9659f798df2888848894bd"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job SET labels = (\n SELECT array_agg(DISTINCT l)\n FROM unnest(coalesce(labels, ARRAY[]::TEXT[]) || $2) l\n ) WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fc59d9b911529de75c466593252358581f5716b3b58dc2e6b9b9282c50b1b66b"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT timestamp, name, code_up FROM datatable_migrations WHERE workspace_id = $1 AND datatable = $2 ORDER BY timestamp ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "timestamp",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "code_up",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fcb0cf55f5c9047066a6fdd62bf6f85238cbf189ccf18191493d6357e269d12d"
|
||||
}
|
||||
Generated
+93
-92
@@ -2491,18 +2491,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
version = "0.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
@@ -2510,27 +2510,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
@@ -13746,7 +13746,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -13828,7 +13828,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-ai"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -13861,7 +13861,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -13874,7 +13874,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14012,7 +14012,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14035,7 +14035,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14050,7 +14050,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14076,7 +14076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -14086,7 +14086,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14103,7 +14103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"base64 0.22.1",
|
||||
@@ -14125,7 +14125,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14148,7 +14148,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14164,7 +14164,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14185,7 +14185,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14206,7 +14206,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14220,7 +14220,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -14255,7 +14255,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14280,7 +14280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"flate2",
|
||||
@@ -14298,7 +14298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14320,7 +14320,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14340,7 +14340,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14377,7 +14377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14405,7 +14405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -14417,7 +14417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.9",
|
||||
@@ -14442,7 +14442,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14456,7 +14456,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14473,6 +14473,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"strum",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"windmill-api-auth",
|
||||
@@ -14490,7 +14491,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -14504,7 +14505,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14523,7 +14524,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -14625,7 +14626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -14644,7 +14645,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14659,7 +14660,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -14683,7 +14684,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -14700,7 +14701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -14716,7 +14717,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -14737,7 +14738,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -14768,7 +14769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -14793,7 +14794,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14827,7 +14828,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -14845,7 +14846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -14854,7 +14855,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14866,7 +14867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14878,7 +14879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -14890,7 +14891,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14902,7 +14903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14914,7 +14915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -14925,7 +14926,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14936,7 +14937,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14948,7 +14949,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -14959,7 +14960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -14981,7 +14982,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14993,7 +14994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15007,7 +15008,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15024,7 +15025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15037,7 +15038,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15049,7 +15050,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15067,7 +15068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -15083,7 +15084,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -15099,7 +15100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15110,7 +15111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15149,7 +15150,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -15188,7 +15189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -15199,7 +15200,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15233,7 +15234,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15257,7 +15258,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15290,7 +15291,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-azure"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15323,7 +15324,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15343,7 +15344,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15377,7 +15378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15413,7 +15414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15436,7 +15437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15460,7 +15461,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15484,7 +15485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15519,7 +15520,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15547,7 +15548,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15572,7 +15573,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.0",
|
||||
@@ -15591,7 +15592,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -15701,7 +15702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
@@ -16427,18 +16428,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.52"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
|
||||
checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.52"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
|
||||
checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -87,7 +87,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0de2412ff0734b11e12ba378c9bcc373ff9ae800
|
||||
27672e37df5d9dfde94f19963d5ffcdf8dd5448c
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE datatable_migrations;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- SQL migrations defined per data table within a workspace.
|
||||
-- `datatable` is the target data table name, `name` is the migration name
|
||||
-- (e.g. add_index_to_customers), and `timestamp` is the migration version
|
||||
-- (YYYYMMDDHHMMSS), recorded as `version` in the data table's `_wm_migrations`
|
||||
-- table once applied.
|
||||
CREATE TABLE datatable_migrations (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
datatable VARCHAR(255) NOT NULL,
|
||||
timestamp BIGINT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code_up TEXT NOT NULL,
|
||||
code_down TEXT,
|
||||
PRIMARY KEY (workspace_id, datatable, timestamp)
|
||||
);
|
||||
|
||||
-- No standalone index: the (workspace_id, datatable, timestamp) primary-key btree
|
||||
-- already serves both `WHERE workspace_id = $1` and `WHERE workspace_id = $1 AND
|
||||
-- datatable = $2` lookups via its leading columns.
|
||||
|
||||
GRANT ALL ON datatable_migrations TO windmill_user;
|
||||
GRANT ALL ON datatable_migrations TO windmill_admin;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace DROP COLUMN dev_workspace_label;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Cosmetic display label for a dev workspace: NULL/'dev' render as "dev", 'staging' renders as "stg".
|
||||
-- Only meaningful when is_dev_workspace = true; changes nothing about behavior (locking, promote and
|
||||
-- compare all key off is_dev_workspace / parent_workspace_id). The value is validated in the handler.
|
||||
ALTER TABLE workspace ADD COLUMN dev_workspace_label VARCHAR;
|
||||
+24
-24
@@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6272,7 +6272,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6284,7 +6284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6293,7 +6293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6305,7 +6305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6317,7 +6317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6329,7 +6329,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6341,7 +6341,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6353,7 +6353,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6364,7 +6364,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6375,7 +6375,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6387,7 +6387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6398,7 +6398,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6420,7 +6420,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6432,7 +6432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6446,7 +6446,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6463,7 +6463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6476,7 +6476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6488,7 +6488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6506,7 +6506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6522,7 +6522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6538,7 +6538,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6570,7 +6570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6581,7 +6581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.750.0"
|
||||
version = "1.751.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
@@ -528,6 +528,16 @@ pub struct PipelineAnnotations {
|
||||
pub column_lineage: Vec<ColumnLineage>,
|
||||
pub macros: bool,
|
||||
pub use_libs: Vec<String>,
|
||||
// `// mute <asset>` — suppress the auto-derived cascade edge for a read
|
||||
// that would otherwise trigger this script (a lookup / slowly-changing
|
||||
// dimension you read every run but don't want to re-run on). Only Asset
|
||||
// specs are stored; native trigger kinds are never auto-derived, so
|
||||
// muting them is meaningless.
|
||||
pub mute: Vec<TriggerSpec>,
|
||||
// `// mute all` — opt out of auto-derivation entirely for this script.
|
||||
// Falls back to explicit-`// on`-only semantics. Explicit `// on` edges
|
||||
// are unaffected.
|
||||
pub mute_all: bool,
|
||||
}
|
||||
|
||||
impl ParseAssetsOutput {
|
||||
@@ -956,6 +966,23 @@ pub fn parse_pipeline_annotations(code: &str) -> PipelineAnnotations {
|
||||
continue;
|
||||
}
|
||||
|
||||
// `// mute all` opts out of auto-derived cascade edges entirely;
|
||||
// `// mute <asset>` suppresses the one edge. Only asset refs are
|
||||
// muteable — native trigger kinds are never auto-derived. Checked
|
||||
// before the generic `on`/asset shorthand (a complete word, so
|
||||
// prose like `// muted for now` never matches).
|
||||
if let Some(after_kw) = consume_keyword(rest, "mute") {
|
||||
let arg = after_kw.trim();
|
||||
if arg == "all" {
|
||||
out.mute_all = true;
|
||||
} else if let Some(spec @ TriggerSpec::Asset { .. }) = parse_trigger_spec(arg) {
|
||||
if !out.mute.contains(&spec) {
|
||||
out.mute.push(spec);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// `data_test` is checked before `on`/asset shorthands and is a complete
|
||||
// word (so it never collides with the `// test:` CI annotation, which
|
||||
// has no whitespace after `test`). Accumulates — every well-formed line
|
||||
|
||||
@@ -769,5 +769,109 @@
|
||||
"tag": null,
|
||||
"retry": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mute suppresses a single ducklake read edge",
|
||||
"code": "// pipeline\n// mute ducklake://main.orders\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null,
|
||||
"mute": [
|
||||
"ducklake:main.orders"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mute all opts out of all auto-derivation",
|
||||
"code": "// pipeline\n// mute all\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null,
|
||||
"mute_all": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mute accumulates in order and dedups",
|
||||
"code": "// pipeline\n// mute ducklake://main.a\n// mute s3://raw/b\n// mute ducklake://main.a\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null,
|
||||
"mute": [
|
||||
"ducklake:main.a",
|
||||
"s3object:raw/b"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mute of a native trigger kind is dropped (only assets are muteable)",
|
||||
"code": "// pipeline\n// mute kafka\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mute prose without an asset ref never false-positives",
|
||||
"code": "// pipeline\n// muted for now\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mute all coexists with explicit on edges",
|
||||
"code": "// pipeline\n// mute all\n// on ducklake://main.orders\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [
|
||||
"ducklake:main.orders"
|
||||
],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null,
|
||||
"mute_all": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "on asset ref strips trailing key=value opts",
|
||||
"code": "// pipeline\n// on ducklake://main.orders debounce=60s\nselect 1",
|
||||
"expected": {
|
||||
"in_pipeline": true,
|
||||
"asset_triggers": [
|
||||
"ducklake:main.orders"
|
||||
],
|
||||
"native_triggers": [],
|
||||
"partition": null,
|
||||
"freshness": null,
|
||||
"tag": null,
|
||||
"retry": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -53,6 +53,13 @@ struct Expected {
|
||||
// `// use <lib_path>` accumulation, declaration order, deduped. Absent === [].
|
||||
#[serde(default)]
|
||||
use_libs: Vec<String>,
|
||||
// `// mute <asset>` accumulation as `kind:path`, declaration order, deduped.
|
||||
// Absent === [].
|
||||
#[serde(default)]
|
||||
mute: Vec<String>,
|
||||
// `// mute all` marker. Absent === false.
|
||||
#[serde(default)]
|
||||
mute_all: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -251,5 +258,18 @@ fn pipeline_annotation_fixtures_match() {
|
||||
|
||||
assert_eq!(got.macros, f.expected.macros, "{ctx}: macros");
|
||||
assert_eq!(got.use_libs, f.expected.use_libs, "{ctx}: use_libs");
|
||||
|
||||
let mute: Vec<String> = got
|
||||
.mute
|
||||
.iter()
|
||||
.filter_map(|t| match t {
|
||||
TriggerSpec::Asset { asset_kind, path, .. } => {
|
||||
Some(format!("{}:{}", kind_str(*asset_kind), path))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(mute, f.expected.mute, "{ctx}: mute");
|
||||
assert_eq!(got.mute_all, f.expected.mute_all, "{ctx}: mute_all");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,6 +578,7 @@ fn print_help() {
|
||||
println!(" JSON_FMT = false Output logs in JSON instead of logfmt");
|
||||
println!(" METRICS_ADDR = None (EE only) Prometheus metrics addr at /metrics; set \"true\" to use :8001");
|
||||
println!(" SUPERADMIN_SECRET = None Virtual superadmin token (server)");
|
||||
println!(" NO_AUTH = false Bypass all auth; every request acts as the admin@windmill.dev superadmin (only behind a trusted gateway; ignored when CLOUD_HOSTED)");
|
||||
println!(" LICENSE_KEY = None (EE only) Enterprise license key (workers require valid key)");
|
||||
println!(" RUN_UPDATE_CA_CERTIFICATE_AT_START = false Run system CA update at startup");
|
||||
println!(" RUN_UPDATE_CA_CERTIFICATE_PATH = /usr/sbin/update-ca-certificates Path to CA update tool");
|
||||
@@ -641,6 +642,15 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
println!("Running in MCP mode");
|
||||
}
|
||||
|
||||
if *windmill_common::worker::NO_AUTH {
|
||||
println!("############################################################");
|
||||
println!("# NO_AUTH mode is ENABLED: authentication is fully #");
|
||||
println!("# bypassed and every request is treated as the #");
|
||||
println!("# admin@windmill.dev superadmin. Only run this behind a #");
|
||||
println!("# trusted authenticating gateway on a private network. #");
|
||||
println!("############################################################");
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
|
||||
println!("jemalloc enabled");
|
||||
|
||||
|
||||
@@ -131,6 +131,13 @@ impl AuthCache {
|
||||
w_id: Option<String>,
|
||||
token: &str,
|
||||
) -> Option<OptJobAuthed> {
|
||||
// In no-auth mode there are no real tokens: resolve directly as the
|
||||
// admin superadmin so direct cache callers (e.g. get_all_runnables,
|
||||
// which re-validates the request token per workspace) don't reject the
|
||||
// fabricated token.
|
||||
if is_no_auth() {
|
||||
return Some(OptJobAuthed { authed: no_auth_admin_authed(), job_id: None });
|
||||
}
|
||||
let key = (
|
||||
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
|
||||
token.to_string(),
|
||||
@@ -598,6 +605,12 @@ where
|
||||
let tokened = Self { token };
|
||||
parts.extensions.insert(tokened.clone());
|
||||
Ok(tokened)
|
||||
} else if is_no_auth() {
|
||||
// In `--no-auth` mode requests carry no token, but handlers that
|
||||
// also require Tokened (e.g. global_whoami) must still resolve.
|
||||
let tokened = Self { token: "no_auth".to_string() };
|
||||
parts.extensions.insert(tokened.clone());
|
||||
Ok(tokened)
|
||||
} else {
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
@@ -677,6 +690,30 @@ fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
|
||||
workspace_id
|
||||
}
|
||||
|
||||
/// `--no-auth` mode: compiled-in `oss` builds, or the `NO_AUTH` runtime flag on
|
||||
/// any build (the runtime flag is force-disabled on CLOUD_HOSTED). When on,
|
||||
/// every request resolves as the admin superadmin so a fronting gateway can
|
||||
/// handle authentication instead.
|
||||
pub fn is_no_auth() -> bool {
|
||||
cfg!(feature = "no_auth") || *windmill_common::worker::NO_AUTH
|
||||
}
|
||||
|
||||
/// The synthetic superadmin identity returned for every request in no-auth mode.
|
||||
fn no_auth_admin_authed() -> ApiAuthed {
|
||||
ApiAuthed {
|
||||
email: "admin@windmill.dev".to_string(),
|
||||
username: "admin".to_string(),
|
||||
is_admin: true,
|
||||
is_operator: false,
|
||||
groups: Vec::new(),
|
||||
folders: Vec::new(),
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves OptJobAuthed from request parts.
|
||||
/// Takes ownership of Parts and returns them back.
|
||||
#[allow(unreachable_code, unused_mut)]
|
||||
@@ -687,21 +724,11 @@ pub async fn resolve_opt_job_authed(
|
||||
return Ok((OptJobAuthed::default(), parts));
|
||||
};
|
||||
|
||||
#[cfg(feature = "no_auth")]
|
||||
{
|
||||
let authed = ApiAuthed {
|
||||
email: "admin@windmill.dev".to_string(),
|
||||
username: "admin".to_string(),
|
||||
is_admin: true,
|
||||
is_operator: false,
|
||||
groups: Vec::new(),
|
||||
folders: Vec::new(),
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
};
|
||||
return Ok((OptJobAuthed { authed, job_id: None }, parts));
|
||||
if is_no_auth() {
|
||||
return Ok((
|
||||
OptJobAuthed { authed: no_auth_admin_authed(), job_id: None },
|
||||
parts,
|
||||
));
|
||||
}
|
||||
|
||||
let already_authed = parts.extensions.get::<OptJobAuthed>().cloned();
|
||||
|
||||
@@ -31,8 +31,8 @@ use scopes::ScopeDefinition;
|
||||
|
||||
// Re-export key auth types and functions
|
||||
pub use auth::{
|
||||
get_end_user_email, invalidate_token_from_cache, AuthCache, ExpiringAuthCache, OptTokened,
|
||||
Tokened, TruncatedTokenWithEmail, AUTH_CACHE,
|
||||
get_end_user_email, invalidate_token_from_cache, is_no_auth, AuthCache, ExpiringAuthCache,
|
||||
OptTokened, Tokened, TruncatedTokenWithEmail, AUTH_CACHE,
|
||||
};
|
||||
|
||||
// ------------ ApiAuthed & OptJobAuthed types ------------
|
||||
|
||||
@@ -45,8 +45,9 @@ use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap;
|
||||
use windmill_common::{
|
||||
assets::{
|
||||
clear_script_triggers, clear_static_asset_usage, clear_static_asset_usage_by_script_hash,
|
||||
insert_script_trigger, parse_duration_secs, parse_pipeline_annotations,
|
||||
replace_static_asset_usage, trigger_spec_to_row, AssetUsageKind, TriggerSpec,
|
||||
derive_pipeline_asset_trigger_refs, insert_script_trigger, parse_duration_secs,
|
||||
parse_pipeline_annotations, replace_static_asset_usage, trigger_spec_to_row,
|
||||
AssetUsageKind, ScriptTriggerKind, TriggerSpec,
|
||||
},
|
||||
error::{self, to_anyhow},
|
||||
min_version::{MIN_VERSION_SUPPORTS_DEBOUNCING, MIN_VERSION_SUPPORTS_DEBOUNCING_V2},
|
||||
@@ -1457,11 +1458,22 @@ async fn create_script_internal<'c>(
|
||||
// fire and the view would be an orphan node in the lineage graph.
|
||||
for (target_kind, path) in m.write_targets() {
|
||||
let kind = windmill_common::assets::asset_kind_from_parser(target_kind);
|
||||
if !a.iter().any(|x| x.kind == kind && x.path == path) {
|
||||
use windmill_common::assets::AssetUsageAccessType;
|
||||
if let Some(existing) = a.iter_mut().find(|x| x.kind == kind && x.path == path) {
|
||||
// The body reads its own managed target (an incremental/merge
|
||||
// model `SELECT`ing from the table it materializes). The runtime
|
||||
// still generates the write, so the effective access is RW — mark
|
||||
// it so, otherwise it stays a plain read and (a) auto-derives a
|
||||
// self-cascade edge back to this producer and (b) shows as a muted
|
||||
// read on the canvas. Both are wrong: it's the script's own output.
|
||||
if existing.access_type != Some(AssetUsageAccessType::W) {
|
||||
existing.access_type = Some(AssetUsageAccessType::RW);
|
||||
}
|
||||
} else {
|
||||
a.push(windmill_common::assets::AssetWithAltAccessType {
|
||||
path,
|
||||
kind,
|
||||
access_type: Some(windmill_common::assets::AssetUsageAccessType::W),
|
||||
access_type: Some(AssetUsageAccessType::W),
|
||||
alt_access_type: None,
|
||||
columns: None,
|
||||
});
|
||||
@@ -2035,6 +2047,63 @@ async fn create_script_internal<'c>(
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Auto-derived cascade edges: within a `// pipeline`, a read of a ducklake
|
||||
// table or s3 object wires the cascade edge straight from the FROM clause,
|
||||
// so `// on <asset>` is only needed for edges inference can't see (dynamic
|
||||
// SQL) or to carry per-edge opts. `// mute <asset>` / `// mute all` opt out.
|
||||
// Explicit `// on` asset edges (inserted just above) win the dedup — they
|
||||
// carry the per-edge debounce, so a derived row must not shadow them.
|
||||
if in_pipeline && !pipeline_annotations.mute_all {
|
||||
let asset_ref = |spec: &TriggerSpec| {
|
||||
trigger_spec_to_row(spec)
|
||||
.filter(|(k, _)| *k == ScriptTriggerKind::Asset)
|
||||
.map(|(_, r)| r)
|
||||
};
|
||||
let explicit_refs: std::collections::HashSet<String> =
|
||||
pipeline_triggers.iter().filter_map(asset_ref).collect();
|
||||
let muted_refs: std::collections::HashSet<String> = pipeline_annotations
|
||||
.mute
|
||||
.iter()
|
||||
.filter_map(asset_ref)
|
||||
.collect();
|
||||
let derived = derive_pipeline_asset_trigger_refs(
|
||||
effective_assets.as_deref().unwrap_or(&[]),
|
||||
&explicit_refs,
|
||||
&muted_refs,
|
||||
pipeline_annotations.mute_all,
|
||||
);
|
||||
// Derived edges have no per-`// on` opts, so they take the script-level
|
||||
// `// debounce` default and `// retry` policy — same as writing a bare
|
||||
// `// on <asset>` would.
|
||||
let derived_debounce_s = pipeline_debounce_default
|
||||
.as_deref()
|
||||
.and_then(parse_duration_secs);
|
||||
let derived_retry_count = pipeline_annotations
|
||||
.retry
|
||||
.as_ref()
|
||||
.map(|r| r.count.min(i16::MAX as u32) as i16);
|
||||
let derived_retry_delay_s = pipeline_annotations
|
||||
.retry
|
||||
.as_ref()
|
||||
.and_then(|r| r.delay.as_deref())
|
||||
.and_then(parse_duration_secs);
|
||||
for trigger_ref in derived {
|
||||
insert_script_trigger(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
AssetUsageKind::Script,
|
||||
&ns.path,
|
||||
ScriptTriggerKind::Asset,
|
||||
&trigger_ref,
|
||||
pipeline_join_all,
|
||||
derived_debounce_s,
|
||||
derived_retry_count,
|
||||
derived_retry_delay_s,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule annotations (`// on schedule`) are marker-only — the binding
|
||||
// lives on the schedule row's own `script_path` field, which the user
|
||||
// creates separately via the schedule editor. No script-create-time
|
||||
|
||||
@@ -1942,8 +1942,10 @@ async fn login(
|
||||
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
||||
Json(Login { email, password }): Json<Login>,
|
||||
) -> Result<String> {
|
||||
#[cfg(feature = "no_auth")]
|
||||
{
|
||||
// In `--no-auth` mode there is no real login; the frontend never needs a
|
||||
// session cookie because every request already resolves as the admin
|
||||
// superadmin (see resolve_opt_job_authed).
|
||||
if windmill_api_auth::is_no_auth() {
|
||||
return Ok("no_auth".to_string());
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
sqlx.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-postgres.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
pub mod datatable_migrations;
|
||||
pub mod deployment_requests;
|
||||
pub mod workspaces;
|
||||
pub mod workspaces_extra;
|
||||
|
||||
@@ -132,6 +132,7 @@ pub fn workspaced_service() -> Router {
|
||||
get(get_datatable_table_schema),
|
||||
)
|
||||
.route("/edit_datatable_config", post(edit_datatable_config))
|
||||
.merge(crate::datatable_migrations::routes())
|
||||
.route("/git_sync_enabled", get(get_git_sync_enabled))
|
||||
.route("/edit_git_sync_config", post(edit_git_sync_config))
|
||||
.route("/edit_git_sync_repository", post(edit_git_sync_repository))
|
||||
@@ -156,6 +157,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/create_fork", post(create_workspace_fork))
|
||||
.route("/attach_dev_workspace", post(attach_dev_workspace))
|
||||
.route("/detach_dev_workspace", post(detach_dev_workspace))
|
||||
.route("/set_dev_workspace_label", post(set_dev_workspace_label))
|
||||
.route("/get_dev_workspace", get(get_dev_workspace))
|
||||
.route("/change_workspace_name", post(change_workspace_name))
|
||||
.route("/change_workspace_color", post(change_workspace_color))
|
||||
@@ -429,6 +431,12 @@ pub struct DucklakeSettings {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct EditDataTableConfig {
|
||||
settings: DataTableSettings,
|
||||
// Data table renames (old -> new) and deletions, tracked client-side by a
|
||||
// stable id, so we can cascade or drop each data table's migrations.
|
||||
#[serde(default)]
|
||||
renames: Vec<crate::datatable_migrations::DatatableRename>,
|
||||
#[serde(default)]
|
||||
deleted_datatables: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
@@ -472,6 +480,10 @@ struct CreateWorkspaceFork {
|
||||
/// the team can work in it. Defaults off; the dev-workspace UI defaults it on.
|
||||
#[serde(default)]
|
||||
copy_members: bool,
|
||||
/// Cosmetic display label for the dev workspace: 'dev' | 'staging'. Purely visual (badge text +
|
||||
/// wording); ignored for non-dev forks. None defaults to 'dev'.
|
||||
#[serde(default)]
|
||||
dev_workspace_label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -501,6 +513,7 @@ struct UserWorkspace {
|
||||
pub operator_settings: Option<Option<serde_json::Value>>,
|
||||
pub parent_workspace_id: Option<String>,
|
||||
pub is_dev_workspace: bool,
|
||||
pub dev_workspace_label: Option<String>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
@@ -678,6 +691,20 @@ async fn exists_workspace(
|
||||
struct DevWorkspaceInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
dev_workspace_label: Option<String>,
|
||||
}
|
||||
|
||||
/// Normalize/validate the cosmetic dev-workspace display label. None or 'dev' both render as "dev";
|
||||
/// 'staging' renders as "stg". Anything else is rejected. Stored explicitly ('dev'/'staging') so it
|
||||
/// round-trips, but a NULL column is treated as 'dev' on the read side too.
|
||||
fn normalize_dev_workspace_label(label: Option<String>) -> Result<Option<String>> {
|
||||
match label.as_deref() {
|
||||
None | Some("dev") => Ok(Some("dev".to_string())),
|
||||
Some("staging") => Ok(Some("staging".to_string())),
|
||||
Some(other) => Err(Error::BadRequest(format!(
|
||||
"invalid dev workspace label '{other}' (expected 'dev' or 'staging')"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// This workspace's active canonical dev workspace, if any. The create-fork UI and the dev-workspace
|
||||
@@ -691,7 +718,7 @@ async fn get_dev_workspace(
|
||||
) -> JsonResult<Option<DevWorkspaceInfo>> {
|
||||
let dev = sqlx::query_as!(
|
||||
DevWorkspaceInfo,
|
||||
"SELECT id, name FROM workspace WHERE parent_workspace_id = $1 AND is_dev_workspace AND deleted = false",
|
||||
"SELECT id, name, dev_workspace_label FROM workspace WHERE parent_workspace_id = $1 AND is_dev_workspace AND deleted = false",
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
@@ -1953,8 +1980,8 @@ pub(crate) async fn resolve_pg_source_checked(
|
||||
}
|
||||
|
||||
/// A temporary file for pg_dump output that is automatically deleted when dropped.
|
||||
struct DumpFile {
|
||||
path: std::path::PathBuf,
|
||||
pub(crate) struct DumpFile {
|
||||
pub(crate) path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl DumpFile {
|
||||
@@ -2001,7 +2028,11 @@ impl Drop for DumpFile {
|
||||
|
||||
/// Run pg_dump against a PgDatabase, writing output to a temp file on disk.
|
||||
/// Returns a DumpFile handle; the file is deleted when the handle is dropped.
|
||||
async fn pg_dump_database(pg_db: &PgDatabase, schema_only: bool) -> Result<DumpFile> {
|
||||
pub(crate) async fn pg_dump_database(
|
||||
pg_db: &PgDatabase,
|
||||
schema_only: bool,
|
||||
exclude_tables: &[&str],
|
||||
) -> Result<DumpFile> {
|
||||
let dump_file = DumpFile::new()?;
|
||||
|
||||
let host = &pg_db.host;
|
||||
@@ -2014,6 +2045,9 @@ async fn pg_dump_database(pg_db: &PgDatabase, schema_only: bool) -> Result<DumpF
|
||||
if schema_only {
|
||||
cmd.arg("--schema-only");
|
||||
}
|
||||
for table in exclude_tables {
|
||||
cmd.arg(format!("--exclude-table={table}"));
|
||||
}
|
||||
cmd.arg("--host")
|
||||
.arg(host)
|
||||
.arg("--port")
|
||||
@@ -2237,7 +2271,7 @@ async fn import_pg_database(
|
||||
}
|
||||
windmill_common::validate_dbname(&target_pg.dbname)?;
|
||||
|
||||
let dump_file = pg_dump_database(&source_pg, schema_only).await?;
|
||||
let dump_file = pg_dump_database(&source_pg, schema_only, &[]).await?;
|
||||
pg_import_dump(&target_pg, &dump_file).await?;
|
||||
|
||||
Ok(format!(
|
||||
@@ -2259,7 +2293,7 @@ async fn export_pg_schema(
|
||||
Json(req): Json<ExportPgSchemaRequest>,
|
||||
) -> Result<String> {
|
||||
let pg = resolve_pg_source_checked(&db, &user_db, &authed, &w_id, &req.source).await?;
|
||||
let dump_file = pg_dump_database(&pg, true).await?;
|
||||
let dump_file = pg_dump_database(&pg, true, &[]).await?;
|
||||
tokio::fs::read_to_string(&dump_file.path)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Failed to read dump file: {}", e)))
|
||||
@@ -2396,13 +2430,57 @@ async fn edit_datatable_config(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, email, .. }: ApiAuthed,
|
||||
Json(new_config): Json<EditDataTableConfig>,
|
||||
Json(mut new_config): Json<EditDataTableConfig>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
let is_superadmin = require_super_admin(&db, &email).await.is_ok();
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let old_datatables: HashMap<String, DataTable> = serde_json::from_value(
|
||||
sqlx::query_scalar!(
|
||||
"SELECT ws.datatable->'datatables' FROM workspace_settings ws WHERE ws.workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Validate every persisted data table name and rename segment before
|
||||
// touching anything, since they become directory segments in migration
|
||||
// storage/export keys (`migrations/datatable/<name>/...`).
|
||||
for name in new_config.settings.datatables.keys() {
|
||||
crate::datatable_migrations::validate_datatable_path_segment(name)?;
|
||||
}
|
||||
for r in &new_config.renames {
|
||||
crate::datatable_migrations::validate_datatable_path_segment(&r.from)?;
|
||||
crate::datatable_migrations::validate_datatable_path_segment(&r.to)?;
|
||||
}
|
||||
|
||||
// Map new name -> old name so a renamed data table inherits the previous
|
||||
// flag instead of being treated as brand new.
|
||||
let rename_src: HashMap<&str, &str> = new_config
|
||||
.renames
|
||||
.iter()
|
||||
.map(|r| (r.to.as_str(), r.from.as_str()))
|
||||
.collect();
|
||||
|
||||
// Migrations opt-in is owned by the enable/disable endpoints, not this config
|
||||
// form: preserve each existing data table's flag, and default brand-new data
|
||||
// tables to enabled.
|
||||
for (name, dt) in new_config.settings.datatables.iter_mut() {
|
||||
let lookup = rename_src
|
||||
.get(name.as_str())
|
||||
.copied()
|
||||
.unwrap_or(name.as_str());
|
||||
dt.migrations_enabled = match old_datatables.get(lookup) {
|
||||
Some(old) => old.migrations_enabled,
|
||||
None => Some(true),
|
||||
};
|
||||
}
|
||||
|
||||
let args_for_audit = format!("{:?}", new_config.settings);
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
@@ -2417,19 +2495,6 @@ async fn edit_datatable_config(
|
||||
|
||||
// Check that non-superadmins are not abusing Instance databases
|
||||
if !is_superadmin {
|
||||
let old_datatables = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT ws.datatable->'datatables' AS datatable_name
|
||||
FROM workspace_settings ws
|
||||
WHERE ws.workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let old_datatables: HashMap<String, DataTable> =
|
||||
serde_json::from_value(old_datatables).unwrap_or_default();
|
||||
for (name, dt) in new_config.settings.datatables.iter() {
|
||||
if dt.database.resource_type == DataTableCatalogResourceType::Instance {
|
||||
let old_dt = old_datatables.get(name);
|
||||
@@ -2458,6 +2523,15 @@ async fn edit_datatable_config(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
crate::datatable_migrations::cascade_datatable_migration_renames_and_deletes(
|
||||
&db,
|
||||
&mut tx,
|
||||
&w_id,
|
||||
&new_config.renames,
|
||||
&new_config.deleted_datatables,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Edit datatable config for workspace {}", &w_id))
|
||||
@@ -3697,7 +3771,7 @@ async fn user_workspaces(
|
||||
let workspaces = sqlx::query_as!(
|
||||
UserWorkspace,
|
||||
"SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,
|
||||
workspace.is_dev_workspace,
|
||||
workspace.is_dev_workspace, workspace.dev_workspace_label,
|
||||
CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,
|
||||
usr.disabled
|
||||
FROM workspace
|
||||
@@ -4048,6 +4122,15 @@ async fn clone_workspace_data(
|
||||
// Clone workspace settings (merge with existing basic settings)
|
||||
update_workspace_settings(tx, source_workspace_id, target_workspace_id).await?;
|
||||
|
||||
// Clone data table migration definitions (the settings above carry the data
|
||||
// table config; this carries their migration history).
|
||||
crate::datatable_migrations::clone_datatable_migrations(
|
||||
tx,
|
||||
source_workspace_id,
|
||||
target_workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Clone workspace environment variables
|
||||
clone_workspace_env(tx, source_workspace_id, target_workspace_id).await?;
|
||||
|
||||
@@ -5187,6 +5270,8 @@ async fn create_workspace_fork_branch(
|
||||
// that second call. Validating early lets a bad request fail before any branch is created.
|
||||
if nw.is_dev_workspace {
|
||||
validate_dev_workspace_id(&nw.id)?;
|
||||
// Reject a bad cosmetic label before any git branch is created (acted on in create_workspace_fork).
|
||||
normalize_dev_workspace_label(nw.dev_workspace_label.clone())?;
|
||||
ensure_dev_parent_is_root(&db, &w_id).await?;
|
||||
// Reject before creating any git branch if the parent already has a dev workspace,
|
||||
// otherwise the deferred branch-creation job leaves a dangling branch on the synced repos.
|
||||
@@ -5418,6 +5503,12 @@ async fn create_workspace_fork(
|
||||
validate_fork_workspace_id(&nw.id)?;
|
||||
}
|
||||
validate_workspace_name(&nw.name)?;
|
||||
// Cosmetic label only applies to dev workspaces; a non-dev fork stores NULL.
|
||||
let dev_workspace_label = if nw.is_dev_workspace {
|
||||
normalize_dev_workspace_label(nw.dev_workspace_label.clone())?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Check the id conflict before the CE workspace-count limit so that
|
||||
// re-using a taken (possibly archived) fork id reports the actual
|
||||
// conflict instead of a misleading "maximum number of workspaces" error.
|
||||
@@ -5495,13 +5586,14 @@ async fn create_workspace_fork(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace
|
||||
(id, name, owner, parent_workspace_id, is_dev_workspace)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
(id, name, owner, parent_workspace_id, is_dev_workspace, dev_workspace_label)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
forked_id,
|
||||
nw.name,
|
||||
authed.email,
|
||||
parent_workspace_id,
|
||||
nw.is_dev_workspace,
|
||||
dev_workspace_label,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -5634,6 +5726,9 @@ struct AttachDevWorkspace {
|
||||
lock_prod_deploy: bool,
|
||||
#[serde(default)]
|
||||
lock_prod_forking: bool,
|
||||
/// Cosmetic display label for the attached dev workspace: 'dev' | 'staging'. None defaults to 'dev'.
|
||||
#[serde(default)]
|
||||
dev_workspace_label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -5687,6 +5782,7 @@ async fn attach_dev_workspace(
|
||||
|
||||
// The id is interpolated into a `wm-fork/<branch>/<id>` branch name like any fork.
|
||||
validate_dev_workspace_id(&dev_w_id)?;
|
||||
let dev_workspace_label = normalize_dev_workspace_label(req.dev_workspace_label.clone())?;
|
||||
|
||||
let dev = sqlx::query!(
|
||||
r#"SELECT parent_workspace_id, deleted FROM workspace WHERE id = $1"#,
|
||||
@@ -5754,9 +5850,10 @@ async fn attach_dev_workspace(
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"UPDATE workspace SET parent_workspace_id = $1, is_dev_workspace = true WHERE id = $2",
|
||||
"UPDATE workspace SET parent_workspace_id = $1, is_dev_workspace = true, dev_workspace_label = $3 WHERE id = $2",
|
||||
&prod_w_id,
|
||||
&dev_w_id
|
||||
&dev_w_id,
|
||||
dev_workspace_label,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -5822,6 +5919,51 @@ async fn attach_dev_workspace(
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SetDevWorkspaceLabel {
|
||||
#[serde(default)]
|
||||
dev_workspace_label: Option<String>,
|
||||
}
|
||||
|
||||
/// Change the cosmetic display label ('dev' | 'staging') of the current workspace, which must itself
|
||||
/// be a dev workspace. Purely visual (badge text + wording); requires admin of the dev workspace.
|
||||
async fn set_dev_workspace_label(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(req): Json<SetDevWorkspaceLabel>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let label = normalize_dev_workspace_label(req.dev_workspace_label)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
let updated = sqlx::query_scalar!(
|
||||
"UPDATE workspace SET dev_workspace_label = $1 WHERE id = $2 AND is_dev_workspace RETURNING id",
|
||||
label,
|
||||
&w_id,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
if updated.is_none() {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Workspace '{w_id}' is not a dev workspace"
|
||||
)));
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.set_dev_workspace_label",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
label.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(format!("Updated dev workspace label for {w_id}"))
|
||||
}
|
||||
|
||||
/// Reverse [`attach_dev_workspace`] / clear the dev designation: unset the dev flag and remove the
|
||||
/// prod lock. The workspace keeps its `parent_workspace_id` (it remains an ordinary fork).
|
||||
async fn detach_dev_workspace(
|
||||
@@ -7339,6 +7481,7 @@ pub struct CompareSummary {
|
||||
pub folders_changed: usize,
|
||||
pub schedules_changed: usize,
|
||||
pub triggers_changed: usize,
|
||||
pub datatable_migrations_changed: usize,
|
||||
pub conflicts: usize, // Items that are both ahead and behind
|
||||
}
|
||||
|
||||
@@ -7530,6 +7673,15 @@ async fn compare_workspaces(
|
||||
compare_two_folders(&db, &source_workspace_id, &fork_workspace_id, &item.path)
|
||||
.await?,
|
||||
),
|
||||
"datatable_migration" => Some(
|
||||
crate::datatable_migrations::compare_two_datatable_migration(
|
||||
&db,
|
||||
&source_workspace_id,
|
||||
&fork_workspace_id,
|
||||
&item.path,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
// Triggers and schedules are diffed against a hardcoded ignore list
|
||||
// (mode/enabled/server_id/last_server_ping/edited_at/by/error/extra_perms/permissioned_as/email)
|
||||
// so that fork-clones — which differ from the parent only in the runtime
|
||||
@@ -7652,6 +7804,10 @@ async fn compare_workspaces(
|
||||
.iter()
|
||||
.filter(|s| s.kind.ends_with("_trigger"))
|
||||
.count(),
|
||||
datatable_migrations_changed: visible_diffs
|
||||
.iter()
|
||||
.filter(|s| s.kind == "datatable_migration")
|
||||
.count(),
|
||||
conflicts: visible_diffs
|
||||
.iter()
|
||||
.filter(|s| s.ahead > 0 && s.behind > 0)
|
||||
@@ -7957,6 +8113,45 @@ async fn query_visible_items<'c>(
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
"datatable_migration" => {
|
||||
// Match by (datatable, timestamp), not the full path: a migration
|
||||
// keeps its identity across a rename, so the candidate path's
|
||||
// `name` segment can differ from the stored one. Parse each
|
||||
// `<datatable>/<timestamp>_<name>` candidate, probe existence by
|
||||
// (datatable, timestamp), and return the *original* candidate path
|
||||
// so the visibility set stays keyed by the diff's path.
|
||||
let parsed: Vec<(String, i64, String)> = paths_vec
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let (dt, rest) = p.split_once('/')?;
|
||||
let ts = rest.split_once('_')?.0.parse::<i64>().ok()?;
|
||||
Some((dt.to_string(), ts, p.clone()))
|
||||
})
|
||||
.collect();
|
||||
if parsed.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
let dts: Vec<String> = parsed.iter().map(|(d, _, _)| d.clone()).collect();
|
||||
let tss: Vec<i64> = parsed.iter().map(|(_, t, _)| *t).collect();
|
||||
let existing: HashSet<(String, i64)> = sqlx::query!(
|
||||
"SELECT datatable, timestamp FROM datatable_migrations \
|
||||
WHERE workspace_id = $1 AND datatable = ANY($2) AND timestamp = ANY($3)",
|
||||
workspace_id,
|
||||
&dts,
|
||||
&tss,
|
||||
)
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| (r.datatable, r.timestamp))
|
||||
.collect();
|
||||
parsed
|
||||
.into_iter()
|
||||
.filter(|(d, t, _)| existing.contains(&(d.clone(), *t)))
|
||||
.map(|(_, _, p)| p)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
k if TRIGGER_OR_SCHEDULE_TABLES.contains(&k) => {
|
||||
// SAFETY: `kind` comes from a hardcoded allowlist
|
||||
// TRIGGER_OR_SCHEDULE_TABLES, not user input.
|
||||
@@ -8024,10 +8219,10 @@ async fn existing_runnables(
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ItemComparison {
|
||||
has_changes: bool,
|
||||
exists_in_source: bool,
|
||||
exists_in_fork: bool,
|
||||
pub(crate) struct ItemComparison {
|
||||
pub(crate) has_changes: bool,
|
||||
pub(crate) exists_in_source: bool,
|
||||
pub(crate) exists_in_fork: bool,
|
||||
}
|
||||
|
||||
async fn compare_two_scripts(
|
||||
|
||||
@@ -91,9 +91,10 @@ pub(crate) async fn change_workspace_id(
|
||||
.await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace)
|
||||
"INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace, dev_workspace_label)
|
||||
SELECT $1, $2, owner, false, premium,
|
||||
CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5
|
||||
CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5,
|
||||
CASE WHEN $5 THEN dev_workspace_label ELSE NULL END
|
||||
FROM workspace WHERE id = $3",
|
||||
&rw.new_id,
|
||||
&rw.new_name,
|
||||
@@ -1095,7 +1096,10 @@ pub(crate) async fn delete_workspace(
|
||||
// effort: failures are logged — the workspace row is already gone, and broken storage
|
||||
// credentials must not have made it undeletable.
|
||||
for e in cleanup_fork_ducklake_namespaces(&db, &w_id, fork_ducklake_cleanups).await {
|
||||
tracing::warn!("deleted workspace {w_id}: ducklake namespace cleanup: {}", e.msg);
|
||||
tracing::warn!(
|
||||
"deleted workspace {w_id}: ducklake namespace cleanup: {}",
|
||||
e.msg
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(parent) = dev_lock_parent {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.750.0
|
||||
version: 1.751.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1205,6 +1205,9 @@ paths:
|
||||
type: boolean
|
||||
lock_prod_forking:
|
||||
type: boolean
|
||||
dev_workspace_label:
|
||||
type: string
|
||||
enum: [dev, staging]
|
||||
required:
|
||||
- dev_workspace_id
|
||||
responses:
|
||||
@@ -1263,10 +1266,40 @@ paths:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
dev_workspace_label:
|
||||
type: string
|
||||
nullable: true
|
||||
description: "Cosmetic display label ('dev' | 'staging'); null defaults to 'dev'"
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
|
||||
/w/{workspace}/workspaces/set_dev_workspace_label:
|
||||
post:
|
||||
summary: set the cosmetic display label (dev/staging) of this dev workspace
|
||||
operationId: setDevWorkspaceLabel
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
dev_workspace_label:
|
||||
type: string
|
||||
enum: [dev, staging]
|
||||
responses:
|
||||
"200":
|
||||
description: dev workspace label updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/workspaces/exists:
|
||||
post:
|
||||
summary: exists workspace
|
||||
@@ -4598,6 +4631,22 @@ paths:
|
||||
properties:
|
||||
settings:
|
||||
$ref: "#/components/schemas/DataTableSettings"
|
||||
renames:
|
||||
description: data tables renamed in this save, so their migrations cascade
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [from, to]
|
||||
properties:
|
||||
from:
|
||||
type: string
|
||||
to:
|
||||
type: string
|
||||
deleted_datatables:
|
||||
description: data tables removed in this save, so their migrations are deleted
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -4605,6 +4654,307 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/workspaces/run_datatable_migrations/{datatable_name}:
|
||||
post:
|
||||
summary: run pending datatable migrations against a datatable
|
||||
operationId: runDatatableMigrations
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: up_to
|
||||
in: query
|
||||
required: false
|
||||
description: only apply pending migrations up to and including this version
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: only
|
||||
in: query
|
||||
required: false
|
||||
description: apply only this specific migration version, ignoring others
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"200":
|
||||
description: applied migrations
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [applied]
|
||||
properties:
|
||||
applied:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [version, name]
|
||||
properties:
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/rollback_datatable_migrations/{datatable_name}:
|
||||
post:
|
||||
summary: roll back the most recently applied migration on a datatable
|
||||
operationId: rollbackDatatableMigrations
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: only
|
||||
in: query
|
||||
required: false
|
||||
description: roll back this specific applied migration version instead of the latest
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"200":
|
||||
description: rolled back migrations
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [rolled_back]
|
||||
properties:
|
||||
rolled_back:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [version, name]
|
||||
properties:
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/list_datatable_migrations:
|
||||
get:
|
||||
summary: list datatable migrations for a workspace
|
||||
operationId: listDatatableMigrations
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: datatable migrations
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/DatatableMigration"
|
||||
|
||||
/w/{workspace}/workspaces/datatable_migrations_status/{datatable_name}:
|
||||
get:
|
||||
summary: list a datatable's migrations with their applied status
|
||||
operationId: getDatatableMigrationsStatus
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: migrations with status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [enabled, migrations]
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
migrations:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/DatatableMigrationWithStatus"
|
||||
error:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/enable_datatable_migrations/{datatable_name}:
|
||||
post:
|
||||
summary: opt a datatable in to migrations (admins / super admins only)
|
||||
operationId: enableDatatableMigrations
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/disable_datatable_migrations/{datatable_name}:
|
||||
post:
|
||||
summary: opt a datatable out of migrations, deleting all of them (admins / super admins only)
|
||||
operationId: disableDatatableMigrations
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/create_datatable_migration/{datatable_name}:
|
||||
post:
|
||||
summary: create a single datatable migration (version generated server-side)
|
||||
operationId: createDatatableMigration
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name, code_up]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
code_up:
|
||||
type: string
|
||||
code_down:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: created migration
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DatatableMigration"
|
||||
|
||||
/w/{workspace}/workspaces/delete_datatable_migration/{datatable_name}/{timestamp}:
|
||||
delete:
|
||||
summary: delete a single datatable migration definition
|
||||
operationId: deleteDatatableMigration
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: timestamp
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/upsert_datatable_migration/{datatable_name}:
|
||||
post:
|
||||
summary: insert or update a single datatable migration at an explicit version
|
||||
operationId: upsertDatatableMigration
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [timestamp, name, code_up]
|
||||
properties:
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
code_up:
|
||||
type: string
|
||||
code_down:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/generate_initial_datatable_migration/{datatable_name}:
|
||||
post:
|
||||
summary: snapshot the current schema as an already-installed initial migration
|
||||
operationId: generateInitialDatatableMigration
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: datatable_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: created migration
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DatatableMigration"
|
||||
|
||||
/w/{workspace}/workspaces/create_pg_database:
|
||||
post:
|
||||
summary: create a new PostgreSQL database for a datatable
|
||||
@@ -28180,6 +28530,10 @@ components:
|
||||
nullable: true
|
||||
is_dev_workspace:
|
||||
type: boolean
|
||||
dev_workspace_label:
|
||||
type: string
|
||||
nullable: true
|
||||
description: "Cosmetic display label of the dev workspace ('dev' | 'staging'); null defaults to 'dev'"
|
||||
created_by:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -28249,6 +28603,10 @@ components:
|
||||
copy_members:
|
||||
type: boolean
|
||||
description: "Copy the parent's members (users + group memberships) into the fork so the team can work in it"
|
||||
dev_workspace_label:
|
||||
type: string
|
||||
enum: [dev, staging]
|
||||
description: "Cosmetic display label for the dev workspace (badge text + wording only); ignored for non-dev forks"
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
@@ -29026,6 +29384,9 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- resource_type
|
||||
migrations_enabled:
|
||||
type: boolean
|
||||
description: Whether the SQL migrations feature is opted in for this data table
|
||||
forked_from:
|
||||
type: object
|
||||
description: Fork origin info with schema snapshot
|
||||
@@ -29034,6 +29395,40 @@ components:
|
||||
type: object
|
||||
description: Schema snapshot at fork time
|
||||
additionalProperties: true
|
||||
DatatableMigration:
|
||||
type: object
|
||||
required: [datatable, timestamp, name, code_up]
|
||||
properties:
|
||||
datatable:
|
||||
type: string
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
code_up:
|
||||
type: string
|
||||
code_down:
|
||||
type: string
|
||||
DatatableMigrationWithStatus:
|
||||
type: object
|
||||
required: [timestamp, name, code_up, status]
|
||||
properties:
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
code_up:
|
||||
type: string
|
||||
code_down:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- ran
|
||||
- not_run
|
||||
- unknown
|
||||
DataTableSchema:
|
||||
type: object
|
||||
required: [datatable_name, schemas]
|
||||
@@ -29745,6 +30140,7 @@ components:
|
||||
- folders_changed
|
||||
- schedules_changed
|
||||
- triggers_changed
|
||||
- datatable_migrations_changed
|
||||
- conflicts
|
||||
properties:
|
||||
total_diffs:
|
||||
@@ -29783,6 +30179,9 @@ components:
|
||||
triggers_changed:
|
||||
type: integer
|
||||
description: Number of triggers with differences (sum across all trigger kinds)
|
||||
datatable_migrations_changed:
|
||||
type: integer
|
||||
description: Number of data table migrations with differences
|
||||
conflicts:
|
||||
type: integer
|
||||
description: Number of items that are both ahead and behind (conflicts)
|
||||
|
||||
@@ -96,6 +96,14 @@ lazy_static::lazy_static! {
|
||||
pub struct CustomMigrator {
|
||||
inner: PoolConnection<Postgres>,
|
||||
}
|
||||
impl CustomMigrator {
|
||||
/// The connection the migrator already holds (with the migration advisory lock).
|
||||
/// Migration housekeeping runs on it instead of re-acquiring: a second connection
|
||||
/// while this one is held deadlocks a single-connection backend (e.g. embedded pglite).
|
||||
pub fn connection(&mut self) -> &mut PgConnection {
|
||||
&mut *self.inner
|
||||
}
|
||||
}
|
||||
impl Migrate for CustomMigrator {
|
||||
fn ensure_migrations_table(
|
||||
&mut self,
|
||||
@@ -272,7 +280,7 @@ pub async fn migrate(
|
||||
version=20250131115248 OR version=20250902085503 OR version=20250201145630 OR
|
||||
version=20250201145631 OR version=20250201145632 OR version=20251006143821"
|
||||
)
|
||||
.execute(db)
|
||||
.execute(custom_migrator.connection())
|
||||
.await
|
||||
{
|
||||
tracing::info!("Could not remove sqlx migrations: {err:#}");
|
||||
@@ -310,7 +318,7 @@ pub async fn migrate(
|
||||
sqlx::query("DELETE FROM _sqlx_migrations WHERE version = $1 AND checksum != $2")
|
||||
.bind(m.version)
|
||||
.bind(&*m.checksum)
|
||||
.execute(db)
|
||||
.execute(custom_migrator.connection())
|
||||
.await
|
||||
{
|
||||
tracing::info!("Could not clean up stale migration {}: {err:#}", m.version);
|
||||
@@ -340,7 +348,7 @@ pub async fn migrate(
|
||||
}
|
||||
}
|
||||
|
||||
crate::live_migrations::custom_migrations(&mut custom_migrator, db).await?;
|
||||
crate::live_migrations::custom_migrations(&mut custom_migrator).await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
use sqlx::Postgres;
|
||||
use windmill_common::error::Error;
|
||||
|
||||
use crate::db::{CustomMigrator, DB};
|
||||
use crate::db::CustomMigrator;
|
||||
use sqlx::migrate::Migrate;
|
||||
use sqlx::Acquire;
|
||||
use sqlx::Executor;
|
||||
|
||||
pub async fn custom_migrations(migrator: &mut CustomMigrator, db: &DB) -> Result<(), Error> {
|
||||
if let Err(err) = fix_flow_versioning_migration(migrator, db).await {
|
||||
pub async fn custom_migrations(migrator: &mut CustomMigrator) -> Result<(), Error> {
|
||||
if let Err(err) = fix_flow_versioning_migration(migrator).await {
|
||||
tracing::error!("Could not apply flow versioning fix migration: {err:#}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fix_flow_versioning_migration(
|
||||
migrator: &mut CustomMigrator,
|
||||
db: &DB,
|
||||
) -> Result<(), Error> {
|
||||
// Runs on the migrator's held connection (see CustomMigrator::connection): re-acquiring
|
||||
// from the pool here would deadlock a single-connection backend.
|
||||
async fn fix_flow_versioning_migration(migrator: &mut CustomMigrator) -> Result<(), Error> {
|
||||
let has_done_migration = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_flow_versioning_2')",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.fetch_one(migrator.connection())
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -44,14 +44,14 @@ async fn fix_flow_versioning_migration(
|
||||
let has_done_migration = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_flow_versioning_2')",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.fetch_one(migrator.connection())
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_done_migration {
|
||||
let query = include_str!("../../custom_migrations/fix_flow_versioning_2.sql");
|
||||
tracing::info!("Applying fix_flow_versioning_2.sql");
|
||||
let mut tx: sqlx::Transaction<'_, Postgres> = db.begin().await?;
|
||||
let mut tx: sqlx::Transaction<'_, Postgres> = migrator.connection().begin().await?;
|
||||
tx.execute(query).await?;
|
||||
tracing::info!("Applied fix_flow_versioning_2.sql");
|
||||
sqlx::query!(
|
||||
|
||||
@@ -1546,6 +1546,34 @@ pub(crate) async fn tarball_workspace(
|
||||
.await?;
|
||||
}
|
||||
|
||||
{
|
||||
// Data table migrations live in the `datatable_migrations` table; surface
|
||||
// them in the export as `migrations/datatable/<datatable>/<version>_<name>`
|
||||
// .up.sql (and .down.sql when present) so `wmill sync` treats them like any
|
||||
// other workspace item.
|
||||
let migrations = sqlx::query!(
|
||||
"SELECT datatable, timestamp, name, code_up, code_down FROM datatable_migrations \
|
||||
WHERE workspace_id = $1 ORDER BY datatable, timestamp",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
for m in migrations {
|
||||
let base = format!(
|
||||
"migrations/datatable/{}/{}_{}",
|
||||
m.datatable, m.timestamp, m.name
|
||||
);
|
||||
archive
|
||||
.write_to_archive(&m.code_up, &format!("{base}.up.sql"))
|
||||
.await?;
|
||||
if let Some(code_down) = m.code_down {
|
||||
archive
|
||||
.write_to_archive(&code_down, &format!("{base}.down.sql"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
archive.finish().await?;
|
||||
|
||||
let file = tokio::fs::File::open(&file_path).await?;
|
||||
|
||||
@@ -169,6 +169,68 @@ fn is_write_access(access: Option<AssetUsageAccessType>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Kinds whose *read* usage auto-derives a cascade trigger edge inside a
|
||||
/// `// pipeline`. Scoped to the two intra-pipeline data kinds — a ducklake
|
||||
/// table read (the core case) and an s3 object read (file-ingestion
|
||||
/// producers). Resource / datatable / volume reads stay explicit-`// on`:
|
||||
/// a config/lookup read cascading is more often surprising than wanted.
|
||||
fn is_auto_trigger_kind(kind: AssetKind) -> bool {
|
||||
matches!(kind, AssetKind::Ducklake | AssetKind::S3Object)
|
||||
}
|
||||
|
||||
/// Trigger refs auto-derived from a pipeline script's inferred reads, so the
|
||||
/// FROM clause alone wires the cascade edge (no redundant `// on <asset>`).
|
||||
///
|
||||
/// Included: an input read read-*only* (`R`) of a supported kind
|
||||
/// ([`is_auto_trigger_kind`]). The effective access type is
|
||||
/// `access_type.or(alt_access_type)` — same precedence as the persisted
|
||||
/// `asset.usage_access_type` and the frontend mirror's `access_type ??
|
||||
/// alt_access_type`, so a manual read override on an ambiguous parse still
|
||||
/// derives an edge (and the live canvas and the deployed graph agree).
|
||||
/// Excluded, each for a reason:
|
||||
/// - `RW` / `W` — the script also writes the asset; an edge would be a
|
||||
/// self-triggering loop.
|
||||
/// - `None` access — usage is ambiguous (poisoned merge) with no override;
|
||||
/// can't confirm a read, so fail safe and don't cascade.
|
||||
/// - already in `explicit_refs` — the author wrote `// on <asset>`, which
|
||||
/// wins (it carries the per-edge debounce/opts).
|
||||
/// - in `muted_refs` — a `// mute <asset>` opt-out (lookup / SCD input).
|
||||
///
|
||||
/// `mute_all` (from `// mute all`) short-circuits to no derivation, leaving
|
||||
/// only the explicit `// on` edges. Returns canonical refs (e.g.
|
||||
/// `ducklake://main.orders`), deduped, in input order.
|
||||
pub fn derive_pipeline_asset_trigger_refs(
|
||||
assets: &[AssetWithAltAccessType],
|
||||
explicit_refs: &HashSet<String>,
|
||||
muted_refs: &HashSet<String>,
|
||||
mute_all: bool,
|
||||
) -> Vec<String> {
|
||||
if mute_all {
|
||||
return vec![];
|
||||
}
|
||||
let mut out = vec![];
|
||||
let mut seen = HashSet::new();
|
||||
for a in assets {
|
||||
// Effective access mirrors the persisted `usage_access_type` and the
|
||||
// frontend derivation: an explicit parse wins, else the manual override.
|
||||
let access = a.access_type.or(a.alt_access_type);
|
||||
if access != Some(AssetUsageAccessType::R) || !is_auto_trigger_kind(a.kind) {
|
||||
continue;
|
||||
}
|
||||
let Some(prefix) = a.kind.canonical_prefix() else {
|
||||
continue;
|
||||
};
|
||||
let r = format!("{}{}", prefix, a.path);
|
||||
if explicit_refs.contains(&r) || muted_refs.contains(&r) {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(r.clone()) {
|
||||
out.push(r);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Clear and reinsert the full static-asset usage set of a script in one tx,
|
||||
/// invalidating the producer-writes cache at most once and only on a real
|
||||
/// change. The cache (asset_dispatch::ASSET_PRODUCER_WRITES_CACHE) keys a
|
||||
@@ -369,6 +431,134 @@ mod debounce_duration_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod derive_trigger_tests {
|
||||
use super::{derive_pipeline_asset_trigger_refs, AssetKind, AssetUsageAccessType};
|
||||
use std::collections::HashSet;
|
||||
use windmill_types::assets::AssetWithAltAccessType;
|
||||
|
||||
fn asset(
|
||||
kind: AssetKind,
|
||||
path: &str,
|
||||
at: Option<AssetUsageAccessType>,
|
||||
) -> AssetWithAltAccessType {
|
||||
AssetWithAltAccessType {
|
||||
path: path.to_string(),
|
||||
kind,
|
||||
access_type: at,
|
||||
alt_access_type: None,
|
||||
columns: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn derive(assets: &[AssetWithAltAccessType]) -> Vec<String> {
|
||||
derive_pipeline_asset_trigger_refs(assets, &HashSet::new(), &HashSet::new(), false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_ducklake_and_s3_derive_an_edge() {
|
||||
use AssetUsageAccessType::R;
|
||||
let a = [
|
||||
asset(AssetKind::Ducklake, "main.orders", Some(R)),
|
||||
asset(AssetKind::S3Object, "raw/events", Some(R)),
|
||||
];
|
||||
assert_eq!(
|
||||
derive(&a),
|
||||
vec![
|
||||
"ducklake://main.orders".to_string(),
|
||||
"s3://raw/events".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writes_and_rw_are_skipped_to_avoid_self_edges() {
|
||||
use AssetUsageAccessType::*;
|
||||
// W (pure producer) and RW (reads *and* writes the same table — a
|
||||
// self-cascade if edged) both derive nothing.
|
||||
let a = [
|
||||
asset(AssetKind::Ducklake, "main.out", Some(W)),
|
||||
asset(AssetKind::Ducklake, "main.self", Some(RW)),
|
||||
];
|
||||
assert!(derive(&a).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambiguous_access_and_unsupported_kinds_are_skipped() {
|
||||
use AssetUsageAccessType::R;
|
||||
let a = [
|
||||
asset(AssetKind::Ducklake, "main.ambiguous", None), // poisoned merge
|
||||
asset(AssetKind::Resource, "f/db", Some(R)), // out of scope
|
||||
asset(AssetKind::DataTable, "main.dt", Some(R)), // out of scope
|
||||
];
|
||||
assert!(derive(&a).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_read_override_on_ambiguous_parse_derives_an_edge() {
|
||||
use AssetUsageAccessType::{R, W};
|
||||
// Parser can't confirm access (`access_type: None`) but the user manually
|
||||
// overrode it. Effective access = `access_type.or(alt_access_type)`, the
|
||||
// same value persisted to `asset.usage_access_type` and used by the
|
||||
// frontend canvas — so a read override derives an edge (parity, no
|
||||
// silently-vanishing edge on deploy) and a write override does not.
|
||||
let read_override = AssetWithAltAccessType {
|
||||
path: "main.override_r".to_string(),
|
||||
kind: AssetKind::Ducklake,
|
||||
access_type: None,
|
||||
alt_access_type: Some(R),
|
||||
columns: None,
|
||||
};
|
||||
let write_override = AssetWithAltAccessType {
|
||||
path: "main.override_w".to_string(),
|
||||
kind: AssetKind::Ducklake,
|
||||
access_type: None,
|
||||
alt_access_type: Some(W),
|
||||
columns: None,
|
||||
};
|
||||
assert_eq!(
|
||||
derive(&[read_override, write_override]),
|
||||
vec!["ducklake://main.override_r".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_and_muted_refs_are_excluded() {
|
||||
use AssetUsageAccessType::R;
|
||||
let a = [
|
||||
asset(AssetKind::Ducklake, "main.explicit", Some(R)),
|
||||
asset(AssetKind::Ducklake, "main.muted", Some(R)),
|
||||
asset(AssetKind::Ducklake, "main.keep", Some(R)),
|
||||
];
|
||||
let explicit: HashSet<String> = ["ducklake://main.explicit".to_string()].into();
|
||||
let muted: HashSet<String> = ["ducklake://main.muted".to_string()].into();
|
||||
assert_eq!(
|
||||
derive_pipeline_asset_trigger_refs(&a, &explicit, &muted, false),
|
||||
vec!["ducklake://main.keep".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mute_all_derives_nothing() {
|
||||
use AssetUsageAccessType::R;
|
||||
let a = [asset(AssetKind::Ducklake, "main.orders", Some(R))];
|
||||
assert!(
|
||||
derive_pipeline_asset_trigger_refs(&a, &HashSet::new(), &HashSet::new(), true)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_reads_dedup() {
|
||||
use AssetUsageAccessType::R;
|
||||
let a = [
|
||||
asset(AssetKind::Ducklake, "main.orders", Some(R)),
|
||||
asset(AssetKind::Ducklake, "main.orders", Some(R)),
|
||||
];
|
||||
assert_eq!(derive(&a), vec!["ducklake://main.orders".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod trigger_ref_roundtrip_tests {
|
||||
use super::{parse_asset_trigger_ref, trigger_spec_to_row, AssetKind, ScriptTriggerKind};
|
||||
|
||||
@@ -273,6 +273,14 @@ lazy_static::lazy_static! {
|
||||
/// production `app.windmill.dev` cluster, not on staging or self-hosted.
|
||||
pub static ref CLOUD_PRODUCTION_HOST: &'static str = "app.windmill.dev";
|
||||
|
||||
/// `--no-auth` mode: when set, every API request is treated as
|
||||
/// authenticated as the `admin@windmill.dev` superadmin and no login is
|
||||
/// ever required. Meant for self-hosted deployments that front Windmill
|
||||
/// with their own authenticating gateway. Never honored on the managed
|
||||
/// cloud (`CLOUD_HOSTED`), which must always enforce real authentication.
|
||||
pub static ref NO_AUTH: bool = !*CLOUD_HOSTED
|
||||
&& std::env::var("NO_AUTH").ok().is_some_and(|x| x == "1" || x == "true");
|
||||
|
||||
pub static ref CUSTOM_TAGS: Vec<String> = std::env::var("CUSTOM_TAGS")
|
||||
.ok()
|
||||
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<_>>()).unwrap_or_default();
|
||||
|
||||
@@ -164,6 +164,7 @@ pub enum ObjectType {
|
||||
Settings,
|
||||
Key,
|
||||
WorkspaceDependencies,
|
||||
DatatableMigration,
|
||||
}
|
||||
|
||||
pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28719/sync-script-to-git-repo-windmill";
|
||||
@@ -771,6 +772,11 @@ pub struct DataTable {
|
||||
pub database: DataTableDatabase,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub forked_from: Option<DataTableForkedFrom>,
|
||||
/// Whether the SQL-migrations feature is opted in for this data table.
|
||||
/// Absent on data tables created before the feature: treated as enabled only
|
||||
/// when migrations already exist (see `datatable_migrations_enabled`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub migrations_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
|
||||
@@ -24,30 +24,102 @@ pub use git_sync_oss::{
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DeployedObject {
|
||||
Script { hash: ScriptHash, path: String, parent_path: Option<String> },
|
||||
Flow { path: String, parent_path: Option<String>, version: i64 },
|
||||
App { path: String, version: i64, parent_path: Option<String> },
|
||||
RawApp { path: String, version: i64, parent_path: Option<String> },
|
||||
Folder { path: String },
|
||||
Resource { path: String, parent_path: Option<String> },
|
||||
Variable { path: String, parent_path: Option<String> },
|
||||
Schedule { path: String },
|
||||
ResourceType { path: String },
|
||||
User { email: String },
|
||||
Group { name: String },
|
||||
HttpTrigger { path: String, parent_path: Option<String> },
|
||||
WebsocketTrigger { path: String, parent_path: Option<String> },
|
||||
KafkaTrigger { path: String, parent_path: Option<String> },
|
||||
NatsTrigger { path: String, parent_path: Option<String> },
|
||||
PostgresTrigger { path: String, parent_path: Option<String> },
|
||||
MqttTrigger { path: String, parent_path: Option<String> },
|
||||
SqsTrigger { path: String, parent_path: Option<String> },
|
||||
GcpTrigger { path: String, parent_path: Option<String> },
|
||||
AzureTrigger { path: String, parent_path: Option<String> },
|
||||
EmailTrigger { path: String, parent_path: Option<String> },
|
||||
Settings { setting_type: String },
|
||||
Key { key_type: String },
|
||||
WorkspaceDependencies { path: String },
|
||||
Script {
|
||||
hash: ScriptHash,
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
Flow {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
version: i64,
|
||||
},
|
||||
App {
|
||||
path: String,
|
||||
version: i64,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
RawApp {
|
||||
path: String,
|
||||
version: i64,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
Folder {
|
||||
path: String,
|
||||
},
|
||||
Resource {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
Variable {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
Schedule {
|
||||
path: String,
|
||||
},
|
||||
ResourceType {
|
||||
path: String,
|
||||
},
|
||||
User {
|
||||
email: String,
|
||||
},
|
||||
Group {
|
||||
name: String,
|
||||
},
|
||||
HttpTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
WebsocketTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
KafkaTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
NatsTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
PostgresTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
MqttTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
SqsTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
GcpTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
AzureTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
EmailTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
Settings {
|
||||
setting_type: String,
|
||||
},
|
||||
Key {
|
||||
key_type: String,
|
||||
},
|
||||
WorkspaceDependencies {
|
||||
path: String,
|
||||
},
|
||||
/// A single data table migration, identified by `<datatable>/<timestamp>_<name>`.
|
||||
DatatableMigration {
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl DeployedObject {
|
||||
@@ -77,6 +149,7 @@ impl DeployedObject {
|
||||
DeployedObject::Settings { .. } => "settings.yaml".to_string(),
|
||||
DeployedObject::Key { .. } => "encryption_key.yaml".to_string(),
|
||||
DeployedObject::WorkspaceDependencies { path, .. } => path.to_owned(),
|
||||
DeployedObject::DatatableMigration { path } => path.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +191,7 @@ impl DeployedObject {
|
||||
DeployedObject::Settings { .. } => None,
|
||||
DeployedObject::Key { .. } => None,
|
||||
DeployedObject::WorkspaceDependencies { .. } => None,
|
||||
DeployedObject::DatatableMigration { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +221,7 @@ impl DeployedObject {
|
||||
DeployedObject::Settings { .. } => "settings",
|
||||
DeployedObject::Key { .. } => "key",
|
||||
DeployedObject::WorkspaceDependencies { .. } => "workspace_dependencies",
|
||||
DeployedObject::DatatableMigration { .. } => "datatable_migration",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.750.0";
|
||||
export const VERSION = "v1.751.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -9,6 +9,13 @@ import { GlobalOptions } from "../../types.ts";
|
||||
import { runCatalogQuery } from "../../utils/catalog.ts";
|
||||
import { psql as psqlDatatable } from "./psql.ts";
|
||||
import { serve as serveDatatable } from "./serve.ts";
|
||||
import {
|
||||
createMigration,
|
||||
pushLocalMigrations,
|
||||
rollbackMigrations,
|
||||
runMigrations,
|
||||
validateLocalMigrations,
|
||||
} from "../datatable_migrations.ts";
|
||||
|
||||
const DEFAULT_DATATABLE_NAME = "main";
|
||||
|
||||
@@ -41,6 +48,69 @@ async function run(
|
||||
await runCatalogQuery(opts, "datatable", name, sql);
|
||||
}
|
||||
|
||||
function migrateNew(
|
||||
opts: GlobalOptions & { datatable?: string },
|
||||
name: string,
|
||||
) {
|
||||
createMigration(opts.datatable ?? DEFAULT_DATATABLE_NAME, name);
|
||||
}
|
||||
|
||||
async function migrateUp(opts: GlobalOptions & { datatable?: string }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const dt = opts.datatable ?? DEFAULT_DATATABLE_NAME;
|
||||
// Reject malformed local migrations (duplicate timestamps, orphan downs) before
|
||||
// pushing — the same check `wmill sync push` runs — so a duplicate timestamp
|
||||
// can't silently overwrite one migration on upsert.
|
||||
const errors = validateLocalMigrations(new Set([dt]));
|
||||
if (errors.length > 0) {
|
||||
log.error(
|
||||
"Invalid datatable migrations, aborting:\n" +
|
||||
errors.map((e) => ` - ${e}`).join("\n"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// Push any locally-created/edited migration files first (without running
|
||||
// them), so `migrate up` works even before a `wmill sync push`.
|
||||
await pushLocalMigrations(workspace.workspaceId, dt);
|
||||
await runMigrations(workspace.workspaceId, dt);
|
||||
}
|
||||
|
||||
async function migrateDown(opts: GlobalOptions & { datatable?: string }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const dt = opts.datatable ?? DEFAULT_DATATABLE_NAME;
|
||||
await rollbackMigrations(workspace.workspaceId, dt);
|
||||
}
|
||||
|
||||
const migrateCommand = new Command()
|
||||
.description("manage datatable migrations")
|
||||
.command("new", "scaffold a new migration (.up.sql / .down.sql files)")
|
||||
.arguments("<name:string>")
|
||||
.option(
|
||||
"-d --datatable <datatable:string>",
|
||||
"Target datatable (default: main)",
|
||||
)
|
||||
.action(migrateNew as any)
|
||||
.command(
|
||||
"up",
|
||||
"apply all pending migrations to the main datatable (or one via --datatable)",
|
||||
)
|
||||
.option(
|
||||
"-d --datatable <datatable:string>",
|
||||
"Target datatable (default: main)",
|
||||
)
|
||||
.action(migrateUp as any)
|
||||
.command(
|
||||
"down",
|
||||
"roll back the most recent migration on the main datatable (or one via --datatable)",
|
||||
)
|
||||
.option(
|
||||
"-d --datatable <datatable:string>",
|
||||
"Target datatable (default: main)",
|
||||
)
|
||||
.action(migrateDown as any);
|
||||
|
||||
async function create(
|
||||
opts: GlobalOptions & { resource?: string; force?: boolean },
|
||||
name?: string,
|
||||
@@ -124,6 +194,7 @@ const command = new Command()
|
||||
"Output only the final result as JSON. Useful for scripting.",
|
||||
)
|
||||
.action(run as any)
|
||||
.command("migrate", migrateCommand)
|
||||
.command(
|
||||
"create",
|
||||
"register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>",
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as log from "../core/log.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as wmill from "../../gen/services.gen.ts";
|
||||
import { readTextFile } from "../utils/utils.ts";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
|
||||
// Migrations live under <cwd>/migrations/datatable/<datatable>/, one folder per
|
||||
// target data table, as `<timestamp>_<name>.up.sql` (and optional `.down.sql`).
|
||||
// They are synced as ordinary workspace files (see the workspace tarball export
|
||||
// and the `datatable_migration` handling in sync.ts); this module only holds the
|
||||
// `wmill datatable migrate` command helpers and the per-file push primitive.
|
||||
const MIGRATIONS_DIR = path.join("migrations", "datatable");
|
||||
|
||||
// Migration names map directly onto file names and the DB `name` column.
|
||||
const MIGRATION_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
/** Current UTC time as a YYYYMMDDHHMMSS migration version. */
|
||||
function migrationTimestamp(): string {
|
||||
const d = new Date();
|
||||
const p = (n: number) => String(n).padStart(2, "0");
|
||||
return (
|
||||
`${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}` +
|
||||
`${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A migration version unique within a data table folder: the current UTC
|
||||
* timestamp bumped past any existing version, so two migrations scaffolded in
|
||||
* the same second don't collide on the `(datatable, timestamp)` identity used to
|
||||
* upsert them.
|
||||
*/
|
||||
function nextMigrationTimestamp(dir: string): string {
|
||||
const now = Number(migrationTimestamp());
|
||||
let max = 0;
|
||||
if (fs.existsSync(dir)) {
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
const m = file.match(/^(\d+)_.*\.(up|down)\.sql$/);
|
||||
if (m) max = Math.max(max, Number(m[1]));
|
||||
}
|
||||
}
|
||||
return String(max >= now ? max + 1 : now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scaffold a new migration under migrations/datatable/<datatable>/ as empty
|
||||
* `<timestamp>_<name>.up.sql` and `.down.sql` files. Purely local — no network.
|
||||
*/
|
||||
export function createMigration(datatable: string, name: string): void {
|
||||
if (!MIGRATION_NAME_RE.test(name)) {
|
||||
throw new Error(
|
||||
`Invalid migration name '${name}': use only letters, digits, '_' and '-'`,
|
||||
);
|
||||
}
|
||||
const dir = path.join(process.cwd(), MIGRATIONS_DIR, datatable);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const timestamp = nextMigrationTimestamp(dir);
|
||||
const base = `${timestamp}_${name}`;
|
||||
const up = path.join(dir, `${base}.up.sql`);
|
||||
const down = path.join(dir, `${base}.down.sql`);
|
||||
// Frame the body in an explicit transaction so it applies atomically, matching
|
||||
// the template the UI's "New migration" modal seeds.
|
||||
const template = (direction: string) =>
|
||||
`-- ${direction} migration: ${name}\nBEGIN;\n\n-- Add your migration here\n\nEND;\n`;
|
||||
fs.writeFileSync(up, template("up"), "utf-8");
|
||||
fs.writeFileSync(down, template("down"), "utf-8");
|
||||
|
||||
log.info(
|
||||
colors.green(`Created migration ${base} in ${MIGRATIONS_DIR}/${datatable}/`),
|
||||
);
|
||||
for (const f of [up, down]) {
|
||||
log.info(colors.gray(` ${path.relative(process.cwd(), f)}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the workspace's pending migrations to a data table (forwards migrations
|
||||
* recorded in `_wm_migrations`). Mirrors `wmill datatable migrate up`.
|
||||
*/
|
||||
export async function runMigrations(
|
||||
workspace: string,
|
||||
datatableName: string,
|
||||
): Promise<void> {
|
||||
const result = await wmill.runDatatableMigrations({
|
||||
workspace,
|
||||
datatableName,
|
||||
});
|
||||
const applied = result.applied ?? [];
|
||||
if (applied.length === 0) {
|
||||
log.info(colors.gray(`No pending migrations to run on '${datatableName}'`));
|
||||
return;
|
||||
}
|
||||
log.info(
|
||||
colors.green(`Applied ${applied.length} migration(s) to '${datatableName}':`),
|
||||
);
|
||||
for (const m of applied) {
|
||||
log.info(colors.gray(` ${m.version} ${m.name}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll back the most recently applied migration on a data table (one step).
|
||||
* Mirrors `wmill datatable migrate down`.
|
||||
*/
|
||||
export async function rollbackMigrations(
|
||||
workspace: string,
|
||||
datatableName: string,
|
||||
): Promise<void> {
|
||||
const result = await wmill.rollbackDatatableMigrations({
|
||||
workspace,
|
||||
datatableName,
|
||||
});
|
||||
const rolledBack = result.rolled_back ?? [];
|
||||
if (rolledBack.length === 0) {
|
||||
log.info(
|
||||
colors.gray(`No applied migrations to roll back on '${datatableName}'`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const m of rolledBack) {
|
||||
log.info(
|
||||
colors.green(`Rolled back migration ${m.version} ${m.name} on '${datatableName}'`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the on-disk migration files for the given data tables (or all of
|
||||
* them when `datatables` is omitted). Returns a list of human-readable problems;
|
||||
* an empty list means the migrations are well-formed. Two states are invalid:
|
||||
* - two up (or two down) files sharing the same timestamp, which collide on the
|
||||
* `(datatable, timestamp)` identity used to upsert; and
|
||||
* - a `.down.sql` with no matching `.up.sql` (an up file is mandatory).
|
||||
*/
|
||||
export function validateLocalMigrations(datatables?: Set<string>): string[] {
|
||||
const errors: string[] = [];
|
||||
const root = path.join(process.cwd(), MIGRATIONS_DIR);
|
||||
if (!fs.existsSync(root)) return errors;
|
||||
|
||||
for (const datatable of fs.readdirSync(root)) {
|
||||
if (datatables && !datatables.has(datatable)) continue;
|
||||
const dtDir = path.join(root, datatable);
|
||||
if (!fs.statSync(dtDir).isDirectory()) continue;
|
||||
|
||||
const upNamesByTs = new Map<number, string[]>();
|
||||
const downNamesByTs = new Map<number, string[]>();
|
||||
const upBases = new Set<string>();
|
||||
const downBases: { ts: number; name: string }[] = [];
|
||||
|
||||
for (const file of fs.readdirSync(dtDir)) {
|
||||
const m = file.match(/^(\d+)_(.*)\.(up|down)\.sql$/);
|
||||
if (!m) continue;
|
||||
const ts = Number(m[1]);
|
||||
const name = m[2];
|
||||
if (m[3] === "up") {
|
||||
(upNamesByTs.get(ts) ?? upNamesByTs.set(ts, []).get(ts)!).push(name);
|
||||
upBases.add(`${ts}_${name}`);
|
||||
} else {
|
||||
(downNamesByTs.get(ts) ?? downNamesByTs.set(ts, []).get(ts)!).push(name);
|
||||
downBases.push({ ts, name });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [ts, names] of upNamesByTs) {
|
||||
if (names.length > 1) {
|
||||
errors.push(
|
||||
`${datatable}: ${names.length} up migrations share timestamp ${ts} (${names.join(", ")})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const [ts, names] of downNamesByTs) {
|
||||
if (names.length > 1) {
|
||||
errors.push(
|
||||
`${datatable}: ${names.length} down migrations share timestamp ${ts} (${names.join(", ")})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const d of downBases) {
|
||||
if (!upBases.has(`${d.ts}_${d.name}`)) {
|
||||
errors.push(
|
||||
`${datatable}: ${d.ts}_${d.name}.down.sql has no matching ${d.ts}_${d.name}.up.sql`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a single migration to the workspace based on the current on-disk state of
|
||||
* its `<datatable>/<timestamp>_<name>.up.sql` file: upsert it when the up file
|
||||
* exists, otherwise delete it. Called by `wmill sync push` for each changed
|
||||
* `datatable_migration` file.
|
||||
*/
|
||||
export async function pushMigrationFromDisk(
|
||||
workspace: string,
|
||||
m: { datatable: string; timestamp: number },
|
||||
): Promise<void> {
|
||||
const dir = path.join(process.cwd(), MIGRATIONS_DIR, m.datatable);
|
||||
// Find the up file for this timestamp regardless of its name segment. A rename
|
||||
// (`123_old.up.sql` -> `123_new.up.sql`) keeps the (datatable, timestamp)
|
||||
// identity but changes the name; the diff sorter may process the deleted old
|
||||
// path before the added new one, so keying off the passed name would delete
|
||||
// the record. Scanning by timestamp upserts the surviving file instead.
|
||||
const upFile = fs.existsSync(dir)
|
||||
? fs.readdirSync(dir).find((f) => {
|
||||
const parsed = f.match(/^(\d+)_(.*)\.up\.sql$/);
|
||||
return parsed !== null && Number(parsed[1]) === m.timestamp;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (upFile === undefined) {
|
||||
log.info(colors.red(`Deleting datatable_migration ${m.datatable}/${m.timestamp}`));
|
||||
await wmill.deleteDatatableMigration({
|
||||
workspace,
|
||||
datatableName: m.datatable,
|
||||
timestamp: m.timestamp,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const name = upFile.match(/^(\d+)_(.*)\.up\.sql$/)![2];
|
||||
const base = `${m.timestamp}_${name}`;
|
||||
const code_up = await readTextFile(path.join(dir, upFile));
|
||||
const downPath = path.join(dir, `${base}.down.sql`);
|
||||
const code_down = fs.existsSync(downPath) ? await readTextFile(downPath) : undefined;
|
||||
|
||||
log.info(colors.green(`Pushing datatable_migration ${m.datatable}/${base}`));
|
||||
await wmill.upsertDatatableMigration({
|
||||
workspace,
|
||||
datatableName: m.datatable,
|
||||
requestBody: {
|
||||
timestamp: m.timestamp,
|
||||
name,
|
||||
code_up,
|
||||
...(code_down !== undefined ? { code_down } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the on-disk migrations of a data table to the workspace, so a freshly
|
||||
* created migration file works with `wmill datatable migrate up` even without a
|
||||
* prior `wmill sync push`. Pushes only migrations that are new or edited
|
||||
* (compared against the workspace's current definitions); it never deletes
|
||||
* remote migrations absent on disk and never touches other item kinds.
|
||||
*/
|
||||
export async function pushLocalMigrations(
|
||||
workspace: string,
|
||||
datatableName: string,
|
||||
): Promise<void> {
|
||||
const dir = path.join(process.cwd(), MIGRATIONS_DIR, datatableName);
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
// Local migrations are identified by their `.up.sql` file (the up file is
|
||||
// mandatory); this deliberately ignores files that were only deleted locally.
|
||||
const local: { timestamp: number; name: string }[] = [];
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
const m = file.match(/^(\d+)_(.*)\.up\.sql$/);
|
||||
if (m) local.push({ timestamp: Number(m[1]), name: m[2] });
|
||||
}
|
||||
if (local.length === 0) return;
|
||||
|
||||
const remote = await wmill.listDatatableMigrations({ workspace });
|
||||
const remoteByTs = new Map(
|
||||
remote
|
||||
.filter((r) => r.datatable === datatableName)
|
||||
.map((r) => [r.timestamp, r] as const),
|
||||
);
|
||||
|
||||
for (const { timestamp, name } of local) {
|
||||
const base = `${timestamp}_${name}`;
|
||||
const code_up = await readTextFile(path.join(dir, `${base}.up.sql`));
|
||||
const downPath = path.join(dir, `${base}.down.sql`);
|
||||
const code_down = fs.existsSync(downPath)
|
||||
? await readTextFile(downPath)
|
||||
: undefined;
|
||||
|
||||
const r = remoteByTs.get(timestamp);
|
||||
const unchanged =
|
||||
r !== undefined &&
|
||||
r.name === name &&
|
||||
r.code_up === code_up &&
|
||||
(r.code_down ?? undefined) === code_down;
|
||||
if (unchanged) continue;
|
||||
|
||||
log.info(colors.green(`Pushing datatable_migration ${datatableName}/${base}`));
|
||||
await wmill.upsertDatatableMigration({
|
||||
workspace,
|
||||
datatableName,
|
||||
requestBody: {
|
||||
timestamp,
|
||||
name,
|
||||
code_up,
|
||||
...(code_down !== undefined ? { code_down } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After a push that introduced new migrations, list them and (interactively)
|
||||
* offer to run them, equivalent to `wmill datatable migrate up` on each affected
|
||||
* data table.
|
||||
*/
|
||||
export async function offerToRunNewMigrations(
|
||||
workspace: string,
|
||||
newMigrations: { datatable: string; timestamp: number; name: string }[],
|
||||
opts?: { yes?: boolean; jsonOutput?: boolean },
|
||||
): Promise<void> {
|
||||
if (newMigrations.length === 0) return;
|
||||
|
||||
log.info(colors.green("New migrations were pushed:"));
|
||||
for (const m of newMigrations) {
|
||||
log.info(colors.gray(` ${m.datatable}: ${m.timestamp} ${m.name}`));
|
||||
}
|
||||
|
||||
// Running migrations mutates the data tables, so skip the prompt in
|
||||
// non-interactive contexts (--yes, --json, no TTY).
|
||||
const interactive = !opts?.jsonOutput && !opts?.yes && !!process.stdin.isTTY;
|
||||
if (!interactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldRun = await Confirm.prompt({
|
||||
message: "New migrations were pushed, run them?",
|
||||
default: false,
|
||||
});
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const datatable of new Set(newMigrations.map((m) => m.datatable))) {
|
||||
await runMigrations(workspace, datatable);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { GlobalOptions, isDatatableMigrationPath } from "../../types.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
@@ -54,6 +54,8 @@ async function walkLocalScripts(
|
||||
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
|
||||
ignore(p, isD) ||
|
||||
isFolderResourcePathAnyFormat(p) ||
|
||||
// Datatable migration `.sql` files aren't Windmill scripts.
|
||||
isDatatableMigrationPath(p) ||
|
||||
(isScriptModulePath(p) && !isModuleEntryPoint(p)),
|
||||
false,
|
||||
{},
|
||||
@@ -221,6 +223,8 @@ function categorizeLocalFiles(
|
||||
} else if (
|
||||
exts.some((ext) => p.endsWith(ext)) &&
|
||||
!isFolderResourcePathAnyFormat(p) &&
|
||||
// Datatable migration `.sql` files aren't Windmill scripts.
|
||||
!isDatatableMigrationPath(p) &&
|
||||
!(isScriptModulePath(p) && !isModuleEntryPoint(p))
|
||||
) {
|
||||
scripts.push(p);
|
||||
|
||||
@@ -23,10 +23,17 @@ import {
|
||||
showDiff,
|
||||
extractNativeTriggerInfo,
|
||||
redactEncryptionKey,
|
||||
isDatatableMigrationPath,
|
||||
parseDatatableMigrationPath,
|
||||
} from "../../types.ts";
|
||||
import { downloadZip } from "./pull.ts";
|
||||
import { runLint, printReport, checkMissingLocks } from "../lint/lint.ts";
|
||||
import { pullSharedUi, pushSharedUi } from "../shared_ui.ts";
|
||||
import {
|
||||
pushMigrationFromDisk,
|
||||
offerToRunNewMigrations,
|
||||
validateLocalMigrations,
|
||||
} from "../datatable_migrations.ts";
|
||||
|
||||
import {
|
||||
exts,
|
||||
@@ -2477,7 +2484,8 @@ const isNotWmillFile = (p: string, isDirectory: boolean) => {
|
||||
!p.startsWith("g" + SEP) &&
|
||||
!p.startsWith("users" + SEP) &&
|
||||
!p.startsWith("groups" + SEP) &&
|
||||
!p.startsWith("dependencies" + SEP)
|
||||
!p.startsWith("dependencies" + SEP) &&
|
||||
!p.startsWith("migrations" + SEP)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2488,6 +2496,11 @@ const isNotWmillFile = (p: string, isDirectory: boolean) => {
|
||||
|
||||
try {
|
||||
const typ = getTypeStrFromPath(p);
|
||||
// Datatable migrations live under migrations/datatable/<datatable>/, outside
|
||||
// the u/f/g namespaces, but are valid wmill files.
|
||||
if (typ == "datatable_migration") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typ == "resource-type" ||
|
||||
typ == "settings" ||
|
||||
@@ -2519,7 +2532,8 @@ export const isWhitelisted = (p: string) => {
|
||||
p == "ui" ||
|
||||
p == "users" ||
|
||||
p == "groups" ||
|
||||
p == "dependencies"
|
||||
p == "dependencies" ||
|
||||
p == "migrations"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2614,6 +2628,11 @@ interface ChangeTracker {
|
||||
}
|
||||
|
||||
async function addToChangedIfNotExists(p: string, tracker: ChangeTracker) {
|
||||
// Datatable migration .sql files are not scripts; they're synced via the
|
||||
// dedicated datatable_migration handler in the push loop.
|
||||
if (isDatatableMigrationPath(p)) {
|
||||
return;
|
||||
}
|
||||
const isScript = exts.some((e) => p.endsWith(e)) && !isFileResource(p) && !isFilesetResource(p);
|
||||
if (isScript) {
|
||||
if (isFlowPath(p)) {
|
||||
@@ -3119,7 +3138,7 @@ export async function pull(
|
||||
change.path.endsWith(".json")
|
||||
) {
|
||||
log.info(
|
||||
`Editing ${getTypeStrFromPath(change.path)} ${targetPath}${
|
||||
`Editing ${changeTypeLabel(change.path)}${targetPath}${
|
||||
targetPath !== change.path
|
||||
? colors.gray(` (workspace-specific override for ${change.path})`)
|
||||
: ""
|
||||
@@ -3137,7 +3156,7 @@ export async function pull(
|
||||
if (opts.stateful) {
|
||||
await mkdir(path.dirname(stateTarget), { recursive: true });
|
||||
log.info(
|
||||
`Adding ${getTypeStrFromPath(change.path)} ${targetPath}${
|
||||
`Adding ${changeTypeLabel(change.path)}${targetPath}${
|
||||
targetPath !== change.path
|
||||
? colors.gray(` (workspace-specific override for ${change.path})`)
|
||||
: ""
|
||||
@@ -3146,7 +3165,7 @@ export async function pull(
|
||||
}
|
||||
await writeFile(target, change.content, "utf-8");
|
||||
log.info(
|
||||
`Writing ${getTypeStrFromPath(change.path)} ${targetPath}${
|
||||
`Writing ${changeTypeLabel(change.path)}${targetPath}${
|
||||
targetPath !== change.path
|
||||
? colors.gray(` (workspace-specific override for ${change.path})`)
|
||||
: ""
|
||||
@@ -3158,7 +3177,7 @@ export async function pull(
|
||||
} else if (change.name === "deleted") {
|
||||
try {
|
||||
log.info(
|
||||
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`,
|
||||
`Deleting ${changeTypeLabel(change.path)}${change.path}`,
|
||||
);
|
||||
await rm(target);
|
||||
if (opts.stateful) {
|
||||
@@ -3350,6 +3369,9 @@ export async function pull(
|
||||
log.warn(`Failed to pull shared UI folder: ${e}`);
|
||||
}
|
||||
|
||||
// Datatable migrations are part of the workspace export now, so they flow
|
||||
// through the normal diff/apply above as `datatable_migration` items.
|
||||
|
||||
// Git-sync deployment-callback mode stops here: branch checkout + pull have
|
||||
// happened, but commit + push are the caller's job. The hub script does
|
||||
// them in-process with `set_gpg_signing_secret` so the agent's pre-warmed
|
||||
@@ -3425,6 +3447,14 @@ export async function gitDeploy(
|
||||
} as any);
|
||||
}
|
||||
|
||||
// Display label for a change's type, with a trailing space. Datatable migrations
|
||||
// are self-describing via their `migrations/datatable/...` path, so they get no
|
||||
// label prefix.
|
||||
function changeTypeLabel(p: string): string {
|
||||
const t = getTypeStrFromPath(p);
|
||||
return t === "datatable_migration" ? "" : `${t} `;
|
||||
}
|
||||
|
||||
function prettyChanges(
|
||||
changes: Change[],
|
||||
specificItems?: SpecificItemsConfig,
|
||||
@@ -3456,7 +3486,7 @@ function prettyChanges(
|
||||
if (change.name === "added") {
|
||||
log.info(
|
||||
colors.green(
|
||||
`+ ${getTypeStrFromPath(change.path)} ` +
|
||||
`+ ${changeTypeLabel(change.path)}` +
|
||||
displayPath +
|
||||
colors.gray(wsNote),
|
||||
) + extraNote,
|
||||
@@ -3464,7 +3494,7 @@ function prettyChanges(
|
||||
} else if (change.name === "deleted") {
|
||||
log.info(
|
||||
colors.red(
|
||||
`- ${getTypeStrFromPath(change.path)} ` +
|
||||
`- ${changeTypeLabel(change.path)}` +
|
||||
displayPath +
|
||||
colors.gray(wsNote),
|
||||
),
|
||||
@@ -3473,7 +3503,7 @@ function prettyChanges(
|
||||
const changeType = getTypeStrFromPath(change.path);
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`~ ${changeType} ` +
|
||||
`~ ${changeTypeLabel(change.path)}` +
|
||||
displayPath +
|
||||
colors.gray(wsNote) +
|
||||
(change.codebase ? ` (codebase changed)` : ""),
|
||||
@@ -4221,6 +4251,24 @@ export async function push(
|
||||
));
|
||||
}
|
||||
|
||||
// Reject malformed datatable migrations (duplicate timestamps, orphan downs)
|
||||
// before touching the remote, scanning only the data tables in this push.
|
||||
const migrationDatatables = new Set(
|
||||
changes
|
||||
.map((c) => parseDatatableMigrationPath(c.path)?.datatable)
|
||||
.filter((d): d is string => !!d),
|
||||
);
|
||||
if (migrationDatatables.size > 0) {
|
||||
const migrationErrors = validateLocalMigrations(migrationDatatables);
|
||||
if (migrationErrors.length > 0) {
|
||||
log.error(
|
||||
"Invalid datatable migrations, aborting push:\n" +
|
||||
migrationErrors.map((e) => ` - ${e}`).join("\n"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!opts.yes &&
|
||||
!(await Confirm.prompt({
|
||||
@@ -4293,6 +4341,21 @@ export async function push(
|
||||
// Cache git branch at the start to avoid repeated execSync calls per change
|
||||
const cachedWsNameForPush = wsNameForFiles || (isGitRepository() ? getCurrentGitBranch() : null);
|
||||
|
||||
// Datatable migrations are two files (.up.sql/.down.sql) for one record, so
|
||||
// dedupe upsert/delete by (datatable, version) across the whole push.
|
||||
const pushedMigrationKeys = new Set<string>();
|
||||
// Migrations newly added by this push (an added .up.sql) — offered to run once
|
||||
// the push has completed.
|
||||
const newDatatableMigrations = changes
|
||||
.filter((c) => c.name === "added")
|
||||
.map((c) => parseDatatableMigrationPath(c.path))
|
||||
.filter((p) => !!p && p.kind === "up")
|
||||
.map((p) => ({
|
||||
datatable: p!.datatable,
|
||||
timestamp: p!.timestamp,
|
||||
name: p!.name,
|
||||
}));
|
||||
|
||||
while (queue.length > 0 || pool.size > 0) {
|
||||
// Fill the pool until we reach the effective parallelism limit.
|
||||
// During the folder-meta phase this is 1 (sequential) so no item change
|
||||
@@ -4320,6 +4383,20 @@ export async function push(
|
||||
}
|
||||
|
||||
for await (const change of changes) {
|
||||
// A datatable migration is one record across two files; upsert/delete
|
||||
// it from disk once (deduped), regardless of which file changed.
|
||||
if (isDatatableMigrationPath(change.path)) {
|
||||
const parsed = parseDatatableMigrationPath(change.path);
|
||||
if (parsed) {
|
||||
const key = `${parsed.datatable}\0${parsed.timestamp}`;
|
||||
if (!pushedMigrationKeys.has(key)) {
|
||||
pushedMigrationKeys.add(key);
|
||||
await pushMigrationFromDisk(workspace.workspaceId, parsed);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let stateTarget = undefined;
|
||||
if (stateful) {
|
||||
try {
|
||||
@@ -4974,6 +5051,16 @@ export async function push(
|
||||
} catch (e) {
|
||||
log.warn(`Failed to push shared UI folder: ${e}`);
|
||||
}
|
||||
try {
|
||||
await offerToRunNewMigrations(workspace.workspaceId, newDatatableMigrations, {
|
||||
yes: opts.yes,
|
||||
jsonOutput: opts.jsonOutput,
|
||||
});
|
||||
} catch (e: any) {
|
||||
log.warn(
|
||||
`Failed to run new datatable migrations: ${e?.body ?? e?.message ?? e}`,
|
||||
);
|
||||
}
|
||||
const lockJobs = await checkServerLockJobs(
|
||||
workspace.workspaceId,
|
||||
pushStartedAt,
|
||||
@@ -5039,6 +5126,7 @@ export async function push(
|
||||
} catch (e) {
|
||||
log.warn(`Failed to push shared UI folder: ${e}`);
|
||||
}
|
||||
// No changes pushed, so no new datatable migrations to run.
|
||||
if (opts.jsonOutput) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
deleteItemInWorkspace,
|
||||
getOnBehalfOf,
|
||||
isTriggerOrScheduleKind,
|
||||
parseDatatableMigrationDeployPath,
|
||||
type DeployKind,
|
||||
type DeployProvider,
|
||||
type TriggerDeployKind,
|
||||
} from "../../../windmill-utils-internal/src/deploy.ts";
|
||||
import { offerToRunNewMigrations } from "../datatable_migrations.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider adapter — wraps CLI's standalone API functions
|
||||
@@ -85,6 +87,10 @@ const provider: DeployProvider = {
|
||||
createSchedule: wmill.createSchedule,
|
||||
updateSchedule: wmill.updateSchedule,
|
||||
deleteSchedule: wmill.deleteSchedule,
|
||||
// Datatable migrations
|
||||
listDatatableMigrations: wmill.listDatatableMigrations,
|
||||
upsertDatatableMigration: wmill.upsertDatatableMigration,
|
||||
deleteDatatableMigration: wmill.deleteDatatableMigration,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -530,6 +536,14 @@ async function mergeWorkspaces(
|
||||
// 10. Deploy
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
// Datatable migrations deployed (not deleted) into the target. Deploying a
|
||||
// migration only upserts its definition — the target schema is unchanged until
|
||||
// the migration is run — so offer to run them afterwards (like the push path).
|
||||
const deployedMigrations: {
|
||||
datatable: string;
|
||||
timestamp: number;
|
||||
name: string;
|
||||
}[] = [];
|
||||
|
||||
for (const diff of sorted) {
|
||||
const label = `${diff.kind}:${diff.path}`;
|
||||
@@ -573,6 +587,12 @@ async function mergeWorkspaces(
|
||||
if (result.success) {
|
||||
log.info(colors.green(` ✓ ${label}`));
|
||||
successCount++;
|
||||
if (
|
||||
!itemDeletedInSource &&
|
||||
(diff.kind as DeployKind) === "datatable_migration"
|
||||
) {
|
||||
deployedMigrations.push(parseDatatableMigrationDeployPath(diff.path));
|
||||
}
|
||||
} else {
|
||||
log.info(colors.red(` ✗ ${label}: ${result.error}`));
|
||||
failCount++;
|
||||
@@ -606,6 +626,18 @@ async function mergeWorkspaces(
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 13. Deployed migration definitions don't touch the target schema until run;
|
||||
// offer to run them on the target now (interactive only, like the push path).
|
||||
if (deployedMigrations.length > 0) {
|
||||
try {
|
||||
await offerToRunNewMigrations(workspaceTo, deployedMigrations, {
|
||||
yes: opts.yes,
|
||||
});
|
||||
} catch (e) {
|
||||
log.warn(colors.yellow(`Failed to run deployed migrations: ${e}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { mergeWorkspaces };
|
||||
|
||||
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
|
||||
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
|
||||
// dependency (main → workspace → utils → main) that triggers a TDZ.
|
||||
// Re-exported from main.ts for backwards compatibility.
|
||||
export const VERSION = "1.750.0";
|
||||
export const VERSION = "1.751.0";
|
||||
|
||||
@@ -5561,6 +5561,8 @@ A raw app has three logical parts:
|
||||
|
||||
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
|
||||
|
||||
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
|
||||
|
||||
### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
|
||||
|
||||
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.
|
||||
@@ -6591,6 +6593,13 @@ datatable related commands
|
||||
- \`datatable run <sql:string>\` - run a SQL query on a datatable
|
||||
- \`-n --name <name:string>\` - Datatable name (default: main)
|
||||
- \`-s --silent\` - Output only the final result as JSON. Useful for scripting.
|
||||
- \`datatable migrate\` - manage datatable migrations
|
||||
- \`datatable migrate new <name:string>\` - scaffold a new migration (.up.sql / .down.sql files)
|
||||
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
|
||||
- \`datatable migrate up\` - apply all pending migrations to the main datatable (or one via --datatable)
|
||||
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
|
||||
- \`datatable migrate down\` - roll back the most recent migration on the main datatable (or one via --datatable)
|
||||
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
|
||||
- \`datatable create [name:string]\` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
|
||||
- \`--resource <resource:string>\` - Back the datatable with an existing postgresql resource path instead of the instance database
|
||||
- \`--force\` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
|
||||
@@ -6859,19 +6868,18 @@ Manage jobs (list, inspect, cancel)
|
||||
|
||||
### jobs
|
||||
|
||||
Pull completed and queued jobs from workspace
|
||||
|
||||
**Arguments:** \`[workspace:string]\`
|
||||
|
||||
**Options:**
|
||||
- \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
|
||||
- \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
|
||||
- \`--skip-worker-check\` - Skip checking for active workers before export
|
||||
Manage jobs (import/export)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`jobs pull\`
|
||||
- \`jobs push\`
|
||||
- \`jobs pull [workspace:string]\` - Pull completed and queued jobs from workspace
|
||||
- \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
|
||||
- \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
|
||||
- \`--skip-worker-check\` - Skip checking for active workers before export
|
||||
- \`jobs push [workspace:string]\` - Push completed and queued jobs to workspace
|
||||
- \`-c, --completed-file <file:string>\` - Completed jobs input file (default: completed_jobs.json)
|
||||
- \`-q, --queued-file <file:string>\` - Queued jobs input file (default: queued_jobs.json)
|
||||
- \`--skip-worker-check\` - Skip checking for active workers before import
|
||||
|
||||
### lint
|
||||
|
||||
|
||||
+36
-4
@@ -274,10 +274,10 @@ export function parseFromPath(p: string, content: string): any {
|
||||
return isWorkspaceDependencies(p)
|
||||
? content
|
||||
: p.endsWith(".yaml")
|
||||
? yamlParseContent(p, content)
|
||||
: p.endsWith(".json")
|
||||
? JSON.parse(content)
|
||||
: content;
|
||||
? yamlParseContent(p, content)
|
||||
: p.endsWith(".json")
|
||||
? JSON.parse(content)
|
||||
: content;
|
||||
}
|
||||
export function parseFromFile(p: string): any {
|
||||
if (p.endsWith(".json")) {
|
||||
@@ -288,9 +288,38 @@ export function parseFromFile(p: string): any {
|
||||
throw new Error("Could not read file " + p);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse a `migrations/datatable/<datatable>/<timestamp>_<name>.(up|down).sql`
|
||||
* path into its parts. Returns undefined for any other path.
|
||||
*/
|
||||
export function parseDatatableMigrationPath(p: string):
|
||||
| { datatable: string; timestamp: number; name: string; kind: "up" | "down" }
|
||||
| undefined {
|
||||
const parts = p.split("/");
|
||||
if (
|
||||
parts[0] !== "migrations" ||
|
||||
parts[1] !== "datatable" ||
|
||||
parts.length !== 4
|
||||
)
|
||||
return undefined;
|
||||
const m = parts[3].match(/^(\d+)_(.*)\.(up|down)\.sql$/);
|
||||
if (!m) return undefined;
|
||||
return {
|
||||
datatable: parts[2],
|
||||
timestamp: Number(m[1]),
|
||||
name: m[2],
|
||||
kind: m[3] as "up" | "down",
|
||||
};
|
||||
}
|
||||
|
||||
export function isDatatableMigrationPath(p: string): boolean {
|
||||
return parseDatatableMigrationPath(p) !== undefined;
|
||||
}
|
||||
|
||||
export function getTypeStrFromPath(
|
||||
p: string
|
||||
):
|
||||
| "datatable_migration"
|
||||
| "script"
|
||||
| "variable"
|
||||
| "flow"
|
||||
@@ -316,6 +345,9 @@ export function getTypeStrFromPath(
|
||||
| "settings"
|
||||
| "encryption_key"
|
||||
| "workspace_dependencies" {
|
||||
if (isDatatableMigrationPath(p)) {
|
||||
return "datatable_migration";
|
||||
}
|
||||
if (isScriptModulePath(p)) {
|
||||
return "script";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Schema, SchemaProperty } from "../../bootstrap/common.ts";
|
||||
|
||||
function quotePropName(name: string): string {
|
||||
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
||||
}
|
||||
|
||||
export function compileResourceTypeToTsType(schema: Schema) {
|
||||
function rec(x: { [name: string]: SchemaProperty }, root = false) {
|
||||
let res = "{\n";
|
||||
@@ -10,15 +14,15 @@ export function compileResourceTypeToTsType(schema: Schema) {
|
||||
let i = 0;
|
||||
for (let [name, prop] of entries) {
|
||||
if (prop.type == "object") {
|
||||
res += ` ${name}: ${rec(prop.properties ?? {})}`;
|
||||
res += ` ${quotePropName(name)}: ${rec(prop.properties ?? {})}`;
|
||||
} else if (prop.type == "array") {
|
||||
res += ` ${name}: ${prop?.items?.type ?? "any"}[]`;
|
||||
res += ` ${quotePropName(name)}: ${prop?.items?.type ?? "any"}[]`;
|
||||
} else {
|
||||
let typ = prop?.type ?? "any";
|
||||
if (typ == "integer") {
|
||||
typ = "number";
|
||||
}
|
||||
res += ` ${name}: ${typ}`;
|
||||
res += ` ${quotePropName(name)}: ${typ}`;
|
||||
}
|
||||
i++;
|
||||
if (i < entries.length) {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Unit tests for datatable-migration path parsing and local validation.
|
||||
*
|
||||
* These exercise pure logic with no backend:
|
||||
* - `parseDatatableMigrationPath` recognizes only the
|
||||
* `migrations/datatable/<dt>/<timestamp>_<name>.(up|down).sql` shape.
|
||||
* - `validateLocalMigrations` rejects the two invalid on-disk states a push
|
||||
* must catch: two up (or two down) files sharing a timestamp, and a
|
||||
* `.down.sql` with no matching `.up.sql`.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { parseDatatableMigrationPath } from "../src/types.ts";
|
||||
import { validateLocalMigrations } from "../src/commands/datatable_migrations.ts";
|
||||
|
||||
describe("parseDatatableMigrationPath", () => {
|
||||
test("parses up and down files of the new layout", () => {
|
||||
expect(
|
||||
parseDatatableMigrationPath(
|
||||
"migrations/datatable/mydt/20260101000001_create_users.up.sql",
|
||||
),
|
||||
).toEqual({
|
||||
datatable: "mydt",
|
||||
timestamp: 20260101000001,
|
||||
name: "create_users",
|
||||
kind: "up",
|
||||
});
|
||||
expect(
|
||||
parseDatatableMigrationPath(
|
||||
"migrations/datatable/my-dt/42_x.down.sql",
|
||||
),
|
||||
).toEqual({ datatable: "my-dt", timestamp: 42, name: "x", kind: "down" });
|
||||
});
|
||||
|
||||
test("rejects unrelated, legacy and malformed paths", () => {
|
||||
for (
|
||||
const p of [
|
||||
// legacy top-level layout
|
||||
"datatable_migrations/mydt/20260101000001_x.up.sql",
|
||||
// wrong sub-namespace / depth
|
||||
"migrations/ducklake/mydt/1_x.up.sql",
|
||||
"migrations/datatable/1_x.up.sql",
|
||||
"migrations/datatable/mydt/sub/1_x.up.sql",
|
||||
// not a migration file
|
||||
"migrations/datatable/mydt/notes.txt",
|
||||
"migrations/datatable/mydt/x.up.sql", // no numeric timestamp prefix
|
||||
// unrelated workspace files
|
||||
"f/foo/bar.script.yaml",
|
||||
"u/admin/script.ts",
|
||||
]
|
||||
) {
|
||||
expect(parseDatatableMigrationPath(p)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateLocalMigrations", () => {
|
||||
let prevCwd: string;
|
||||
let tmp: string;
|
||||
|
||||
beforeEach(() => {
|
||||
prevCwd = process.cwd();
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "dtmig-"));
|
||||
process.chdir(tmp);
|
||||
});
|
||||
afterEach(() => {
|
||||
process.chdir(prevCwd);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function write(datatable: string, file: string) {
|
||||
const dir = path.join(tmp, "migrations", "datatable", datatable);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, file), "-- sql\n");
|
||||
}
|
||||
|
||||
test("accepts up+down pairs and up-only migrations", () => {
|
||||
write("mydt", "20260101000001_create_users.up.sql");
|
||||
write("mydt", "20260101000001_create_users.down.sql");
|
||||
write("mydt", "20260101000002_add_email.up.sql"); // down is optional
|
||||
expect(validateLocalMigrations()).toEqual([]);
|
||||
});
|
||||
|
||||
test("flags two up files sharing a timestamp", () => {
|
||||
write("mydt", "20260101000003_foo.up.sql");
|
||||
write("mydt", "20260101000003_bar.up.sql");
|
||||
const errors = validateLocalMigrations();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("20260101000003");
|
||||
});
|
||||
|
||||
test("flags two down files sharing a timestamp", () => {
|
||||
write("mydt", "20260101000004_a.up.sql");
|
||||
write("mydt", "20260101000004_a.down.sql");
|
||||
write("mydt", "20260101000004_b.down.sql");
|
||||
const errors = validateLocalMigrations();
|
||||
// duplicate down + the b.down orphan (no b.up)
|
||||
expect(errors.some((e) => e.includes("down") && e.includes("20260101000004"))).toBe(true);
|
||||
});
|
||||
|
||||
test("flags a down file with no matching up", () => {
|
||||
write("mydt", "20260101000005_orphan.down.sql");
|
||||
const errors = validateLocalMigrations();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("20260101000005_orphan");
|
||||
});
|
||||
|
||||
test("only validates the requested datatables", () => {
|
||||
write("bad", "20260101000006_x.up.sql");
|
||||
write("bad", "20260101000006_y.up.sql"); // duplicate, but in 'bad'
|
||||
write("good", "20260101000007_ok.up.sql");
|
||||
expect(validateLocalMigrations(new Set(["good"]))).toEqual([]);
|
||||
expect(validateLocalMigrations(new Set(["bad"])).length).toBe(1);
|
||||
});
|
||||
|
||||
test("returns no errors when the migrations folder is absent", () => {
|
||||
expect(validateLocalMigrations()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { compileResourceTypeToTsType } from "../src/utils/resource_types.ts";
|
||||
import type { Schema } from "../bootstrap/common.ts";
|
||||
|
||||
// =============================================================================
|
||||
// Resource-type namespace generation (WIN-2132)
|
||||
//
|
||||
// `compileResourceTypeToTsType` renders a JSON Schema into the body of a
|
||||
// TypeScript type used in the generated `rt.d.ts` (RT namespace). JSON Schema
|
||||
// property names are unconstrained, so a name with a colon, hyphen, or space
|
||||
// is legal in the schema but not a valid bare TS identifier. Emitting it raw
|
||||
// produced syntactically invalid output that broke `tsc`. These tests pin that
|
||||
// such names are quoted while plain identifiers stay bare.
|
||||
// =============================================================================
|
||||
|
||||
function schema(properties: Schema["properties"]): Schema {
|
||||
return {
|
||||
$schema: undefined,
|
||||
type: "object",
|
||||
properties,
|
||||
required: [],
|
||||
};
|
||||
}
|
||||
|
||||
test("plain identifiers are emitted without quotes", () => {
|
||||
const out = compileResourceTypeToTsType(
|
||||
schema({
|
||||
host: { type: "string" },
|
||||
_port: { type: "integer" },
|
||||
$ref: { type: "boolean" },
|
||||
})
|
||||
);
|
||||
expect(out).toContain(" host: string");
|
||||
expect(out).toContain(" _port: number");
|
||||
expect(out).toContain(" $ref: boolean");
|
||||
expect(out).not.toContain('"host"');
|
||||
});
|
||||
|
||||
test("non-identifier property names are double-quoted", () => {
|
||||
const out = compileResourceTypeToTsType(
|
||||
schema({
|
||||
"content-type": { type: "string" },
|
||||
"x:api:key": { type: "string" },
|
||||
"with space": { type: "integer" },
|
||||
"3leading": { type: "boolean" },
|
||||
})
|
||||
);
|
||||
expect(out).toContain(' "content-type": string');
|
||||
expect(out).toContain(' "x:api:key": string');
|
||||
expect(out).toContain(' "with space": number');
|
||||
expect(out).toContain(' "3leading": boolean');
|
||||
});
|
||||
|
||||
test("nested object and array property names are quoted too", () => {
|
||||
const out = compileResourceTypeToTsType(
|
||||
schema({
|
||||
"nested-obj": {
|
||||
type: "object",
|
||||
properties: { "inner-key": { type: "string" } },
|
||||
},
|
||||
"arr-field": { type: "array", items: { type: "string" } },
|
||||
})
|
||||
);
|
||||
expect(out).toContain('"nested-obj": {');
|
||||
expect(out).toContain('"inner-key": string');
|
||||
expect(out).toContain('"arr-field": string[]');
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.2",
|
||||
"description": "Internal utility functions for Windmill",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
||||
@@ -21,6 +21,7 @@ export type DeployKind =
|
||||
| "resource_type"
|
||||
| "folder"
|
||||
| "schedule"
|
||||
| "datatable_migration"
|
||||
| "http_trigger"
|
||||
| "websocket_trigger"
|
||||
| "kafka_trigger"
|
||||
@@ -161,6 +162,24 @@ export interface DeployProvider {
|
||||
requestBody: any;
|
||||
}): Promise<any>;
|
||||
deleteFolder(p: { workspace: string; name: string }): Promise<any>;
|
||||
// Datatable migrations. In the diff, an item's `path` is
|
||||
// `<datatable>/<timestamp>_<name>` (see `parseDatatableMigrationDeployPath`).
|
||||
listDatatableMigrations(p: { workspace: string }): Promise<any>;
|
||||
upsertDatatableMigration(p: {
|
||||
workspace: string;
|
||||
datatableName: string;
|
||||
requestBody: {
|
||||
timestamp: number;
|
||||
name: string;
|
||||
code_up: string;
|
||||
code_down?: string;
|
||||
};
|
||||
}): Promise<any>;
|
||||
deleteDatatableMigration(p: {
|
||||
workspace: string;
|
||||
datatableName: string;
|
||||
timestamp: number;
|
||||
}): Promise<any>;
|
||||
// Triggers — per-kind dispatch is delegated to the implementor so the shared
|
||||
// module doesn't need to know about each of the 9 trigger services.
|
||||
existsTriggerByKind(
|
||||
@@ -299,6 +318,40 @@ function toError(e: unknown): string {
|
||||
return err.body || err.message || String(e);
|
||||
}
|
||||
|
||||
// A datatable-migration diff item's path is `<datatable>/<timestamp>_<name>`
|
||||
// (mirrors the backend, e.g. `mydt/20260101000001_create_users`).
|
||||
export function parseDatatableMigrationDeployPath(path: string): {
|
||||
datatable: string;
|
||||
timestamp: number;
|
||||
name: string;
|
||||
} {
|
||||
const slash = path.indexOf("/");
|
||||
const underscore = slash >= 0 ? path.indexOf("_", slash + 1) : -1;
|
||||
if (slash < 0 || underscore < 0) {
|
||||
throw new Error(`Invalid datatable migration path: ${path}`);
|
||||
}
|
||||
const datatable = path.slice(0, slash);
|
||||
const timestamp = Number(path.slice(slash + 1, underscore));
|
||||
const name = path.slice(underscore + 1);
|
||||
if (!datatable || !Number.isFinite(timestamp) || !name) {
|
||||
throw new Error(`Invalid datatable migration path: ${path}`);
|
||||
}
|
||||
return { datatable, timestamp, name };
|
||||
}
|
||||
|
||||
// The backend rejects `upsertDatatableMigration` when the target data table
|
||||
// hasn't opted in to migrations. Turn that opaque 400 into an explicit,
|
||||
// deploy-context message (falls back to the original error otherwise).
|
||||
function asMigrationsDisabledError(e: unknown, datatable: string): unknown {
|
||||
const msg = (e as { body?: string; message?: string })?.body ?? ''
|
||||
if (typeof msg === "string" && /migrations are not enabled/i.test(msg)) {
|
||||
return new Error(
|
||||
`Data table '${datatable}' has not opted in to migrations on the target workspace; enable migrations for it there before deploying its migrations.`
|
||||
);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkItemExists
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -325,6 +378,12 @@ export async function checkItemExists(
|
||||
return provider.existsFolder({ workspace, name: folderName(path) });
|
||||
} else if (kind === "schedule") {
|
||||
return provider.existsSchedule({ workspace, path });
|
||||
} else if (kind === "datatable_migration") {
|
||||
const { datatable, timestamp } = parseDatatableMigrationDeployPath(path);
|
||||
const migrations = await provider.listDatatableMigrations({ workspace });
|
||||
return (migrations as { datatable: string; timestamp: number }[]).some(
|
||||
(m) => m.datatable === datatable && m.timestamp === timestamp
|
||||
);
|
||||
} else if (isTriggerKind(kind)) {
|
||||
return provider.existsTriggerByKind(kind, { workspace, path });
|
||||
}
|
||||
@@ -639,6 +698,41 @@ export async function deployItem(
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
} else if (kind === "datatable_migration") {
|
||||
const { datatable, timestamp } = parseDatatableMigrationDeployPath(path);
|
||||
const migrations = await provider.listDatatableMigrations({
|
||||
workspace: workspaceFrom,
|
||||
});
|
||||
const migration = (
|
||||
migrations as {
|
||||
datatable: string;
|
||||
timestamp: number;
|
||||
name: string;
|
||||
code_up: string;
|
||||
code_down?: string;
|
||||
}[]
|
||||
).find((m) => m.datatable === datatable && m.timestamp === timestamp);
|
||||
if (!migration) {
|
||||
throw new Error(
|
||||
`Datatable migration ${path} not found in ${workspaceFrom}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await provider.upsertDatatableMigration({
|
||||
workspace: workspaceTo,
|
||||
datatableName: datatable,
|
||||
requestBody: {
|
||||
timestamp: migration.timestamp,
|
||||
name: migration.name,
|
||||
code_up: migration.code_up,
|
||||
...(migration.code_down != null
|
||||
? { code_down: migration.code_down }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
throw asMigrationsDisabledError(e, datatable);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown kind: ${kind}`);
|
||||
}
|
||||
@@ -684,6 +778,13 @@ export async function deleteItemInWorkspace(
|
||||
await provider.deleteFolder({ workspace, name: folderName(path) });
|
||||
} else if (kind === "schedule") {
|
||||
await provider.deleteSchedule({ workspace, path });
|
||||
} else if (kind === "datatable_migration") {
|
||||
const { datatable, timestamp } = parseDatatableMigrationDeployPath(path);
|
||||
await provider.deleteDatatableMigration({
|
||||
workspace,
|
||||
datatableName: datatable,
|
||||
timestamp,
|
||||
});
|
||||
} else if (isTriggerKind(kind)) {
|
||||
await provider.deleteTriggerByKind(kind, { workspace, path });
|
||||
} else {
|
||||
@@ -767,6 +868,29 @@ export async function getItemValue(
|
||||
} else if (isTriggerKind(kind)) {
|
||||
const trigger = await provider.getTriggerValue(kind, { workspace, path });
|
||||
return stripTriggerOrScheduleRuntimeFields(trigger);
|
||||
} else if (kind === "datatable_migration") {
|
||||
// Surface the migration SQL so the diff drawer shows the up/down bodies a
|
||||
// reviewer needs to inspect before deploying.
|
||||
const { datatable, timestamp } = parseDatatableMigrationDeployPath(path);
|
||||
const migrations = (await provider.listDatatableMigrations({
|
||||
workspace,
|
||||
})) as {
|
||||
datatable: string;
|
||||
timestamp: number;
|
||||
name: string;
|
||||
code_up: string;
|
||||
code_down?: string;
|
||||
}[];
|
||||
const migration = migrations.find(
|
||||
(m) => m.datatable === datatable && m.timestamp === timestamp
|
||||
);
|
||||
if (migration) {
|
||||
return {
|
||||
name: migration.name,
|
||||
code_up: migration.code_up,
|
||||
code_down: migration.code_down ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Item may not exist
|
||||
|
||||
@@ -35,7 +35,10 @@ RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
|
||||
|
||||
# Oracle DB Client
|
||||
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
|
||||
RUN apt-get -y update && apt-get install -y libaio1
|
||||
# libaio1t64 only ships libaio.so.1t64; Oracle instantclient loads libaio.so.1, so add a compat symlink
|
||||
RUN apt-get -y update && apt-get install -y libaio1t64 \
|
||||
&& libaio="$(find /usr/lib -name 'libaio.so.1t64' -print -quit)" \
|
||||
&& ln -s "$(basename "$libaio")" "$(dirname "$libaio")/libaio.so.1"
|
||||
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
|
||||
|
||||
# Nushell
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEBIAN_IMAGE=debian:bookworm-slim
|
||||
ARG DEBIAN_IMAGE=debian:trixie-slim
|
||||
|
||||
FROM debian:bookworm-slim AS nsjail
|
||||
FROM debian:trixie-slim AS nsjail
|
||||
|
||||
WORKDIR /nsjail
|
||||
|
||||
@@ -9,12 +9,12 @@ RUN apt-get -y update \
|
||||
bison=2:3.8.* \
|
||||
ca-certificates \
|
||||
flex=2.6.* \
|
||||
g++=4:12.2.* \
|
||||
gcc=4:12.2.* \
|
||||
git=1:2.39.* \
|
||||
g++=4:14.2.* \
|
||||
gcc=4:14.2.* \
|
||||
git=1:2.47.* \
|
||||
libprotobuf-dev=3.21.* \
|
||||
libnl-route-3-dev=3.7.* \
|
||||
make=4.3-4.1 \
|
||||
make=4.4.* \
|
||||
pkg-config=1.8.* \
|
||||
protobuf-compiler=3.21.* \
|
||||
&& apt-get clean \
|
||||
@@ -39,7 +39,7 @@ ENV PATH=/usr/local/bin:/root/.local/bin:/tmp/.local/bin:$PATH
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates wget curl git jq unzip unixodbc xmlsec1 gnupg lsb-release libgnutls30 libgcrypt20 \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates wget curl git jq unzip unixodbc xmlsec1 gnupg lsb-release libgnutls30t64 libgcrypt20 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -86,7 +86,7 @@ COPY --from=docker:29-dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
# nsjail runtime deps and binary
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libprotobuf32 libnl-route-3-200 libnl-3-200 \
|
||||
&& apt-get install -y --no-install-recommends libprotobuf32t64 libnl-route-3-200 libnl-3-200 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEBIAN_IMAGE=debian:bookworm-slim
|
||||
ARG DEBIAN_IMAGE=debian:trixie-slim
|
||||
|
||||
FROM debian:bookworm-slim AS nsjail
|
||||
FROM debian:trixie-slim AS nsjail
|
||||
|
||||
WORKDIR /nsjail
|
||||
|
||||
@@ -9,12 +9,12 @@ RUN apt-get -y update \
|
||||
bison=2:3.8.* \
|
||||
ca-certificates \
|
||||
flex=2.6.* \
|
||||
g++=4:12.2.* \
|
||||
gcc=4:12.2.* \
|
||||
git=1:2.39.* \
|
||||
g++=4:14.2.* \
|
||||
gcc=4:14.2.* \
|
||||
git=1:2.47.* \
|
||||
libprotobuf-dev=3.21.* \
|
||||
libnl-route-3-dev=3.7.* \
|
||||
make=4.3-4.1 \
|
||||
make=4.4.* \
|
||||
pkg-config=1.8.* \
|
||||
protobuf-compiler=3.21.* \
|
||||
&& apt-get clean \
|
||||
@@ -39,7 +39,7 @@ ENV PATH=/usr/local/bin:/root/.local/bin:/tmp/.local/bin:$PATH
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates wget curl git jq unzip unixodbc xmlsec1 gnupg lsb-release libgnutls30 libgcrypt20 \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates wget curl git jq unzip unixodbc xmlsec1 gnupg lsb-release libgnutls30t64 libgcrypt20 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -86,7 +86,7 @@ COPY --from=docker:29-dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
# nsjail runtime deps and binary
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libprotobuf32 libnl-route-3-200 libnl-3-200 \
|
||||
&& apt-get install -y --no-install-recommends libprotobuf32t64 libnl-route-3-200 libnl-3-200 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
|
||||
Generated
+57
-118
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.750.0",
|
||||
"version": "1.751.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.750.0",
|
||||
"version": "1.751.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -94,7 +94,7 @@
|
||||
"windmill-parser-wasm-wac": "1.668.6",
|
||||
"windmill-parser-wasm-yaml": "1.593.0",
|
||||
"windmill-sql-datatype-parser-wasm": "1.512.0",
|
||||
"windmill-utils-internal": "^1.7.0",
|
||||
"windmill-utils-internal": "1.8.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.2",
|
||||
"y-monaco": "^0.1.4",
|
||||
@@ -291,7 +291,6 @@
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"js-tokens": "^4.0.0",
|
||||
@@ -307,7 +306,6 @@
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -865,7 +863,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
},
|
||||
@@ -875,21 +872,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
|
||||
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
|
||||
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -898,9 +895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -1392,6 +1389,7 @@
|
||||
"integrity": "sha512-Jer+M7DgIwT5IHfTayb4Iw/fkkxWNmC/mqn/nMh9JrbPbkxmyabfLQnhJ+JDn5HK77f84j34lubO3iqFtYAfMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.3.1",
|
||||
"@floating-ui/dom": "^1.4.5",
|
||||
@@ -1548,6 +1546,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
@@ -1774,8 +1773,8 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@emnapi/core": "1.11.2",
|
||||
"@emnapi/runtime": "1.11.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1912,6 +1911,7 @@
|
||||
"integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
@@ -2007,6 +2007,7 @@
|
||||
"integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.3.1",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -2468,8 +2469,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
@@ -2482,8 +2482,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
|
||||
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.7.1",
|
||||
@@ -2552,6 +2551,7 @@
|
||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
@@ -3079,6 +3079,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3132,6 +3133,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -3277,7 +3279,6 @@
|
||||
"integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3305,7 +3306,6 @@
|
||||
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3386,8 +3386,7 @@
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
|
||||
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
@@ -3514,6 +3513,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -3711,7 +3711,6 @@
|
||||
"integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"camelcase": "^6.3.0",
|
||||
"map-obj": "^4.1.0",
|
||||
@@ -3731,7 +3730,6 @@
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3745,7 +3743,6 @@
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3759,7 +3756,6 @@
|
||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3868,6 +3864,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -4144,7 +4141,6 @@
|
||||
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"import-fresh": "^3.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -4209,7 +4205,6 @@
|
||||
"integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -4397,6 +4392,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz",
|
||||
"integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -4819,6 +4815,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -4918,6 +4915,7 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
@@ -4958,7 +4956,6 @@
|
||||
"integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -4972,7 +4969,6 @@
|
||||
"integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"decamelize": "^1.1.0",
|
||||
"map-obj": "^1.0.0"
|
||||
@@ -4990,7 +4986,6 @@
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5001,7 +4996,6 @@
|
||||
"integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5495,7 +5489,6 @@
|
||||
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
@@ -5593,6 +5586,7 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -6149,7 +6143,6 @@
|
||||
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4.9.1"
|
||||
}
|
||||
@@ -6509,7 +6502,6 @@
|
||||
"integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"global-prefix": "^3.0.0"
|
||||
},
|
||||
@@ -6523,7 +6515,6 @@
|
||||
"integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ini": "^1.3.5",
|
||||
"kind-of": "^6.0.2",
|
||||
@@ -6539,7 +6530,6 @@
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@@ -6589,8 +6579,7 @@
|
||||
"resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
|
||||
"integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
@@ -6673,7 +6662,6 @@
|
||||
"integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -6910,7 +6898,6 @@
|
||||
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -6924,7 +6911,6 @@
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
@@ -6937,8 +6923,7 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html-tags": {
|
||||
"version": "3.3.1",
|
||||
@@ -6946,7 +6931,6 @@
|
||||
"integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -7042,7 +7026,6 @@
|
||||
"integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -7073,7 +7056,6 @@
|
||||
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7143,8 +7125,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
@@ -7249,7 +7230,6 @@
|
||||
"integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7260,7 +7240,6 @@
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7365,8 +7344,7 @@
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -7402,8 +7380,7 @@
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-refs": {
|
||||
"version": "3.0.15",
|
||||
@@ -7600,7 +7577,6 @@
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -8206,8 +8182,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
||||
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
@@ -8275,7 +8250,6 @@
|
||||
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -8326,7 +8300,6 @@
|
||||
"integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
@@ -8567,7 +8540,6 @@
|
||||
"integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/minimist": "^1.2.2",
|
||||
"camelcase-keys": "^7.0.0",
|
||||
@@ -8595,7 +8567,6 @@
|
||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -9330,7 +9301,6 @@
|
||||
"integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"arrify": "^1.0.1",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
@@ -9396,6 +9366,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-api/-/monaco-vscode-editor-api-25.0.0.tgz",
|
||||
"integrity": "sha512-uiY06RTWFo2WZdh6OybkLlDhuG+8LlkjUDpr9/wW55uucqHo4X8fx4XKEtD98cscC+6FKQkbG2yyUiOJ/npHOw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codingame/monaco-vscode-api": "25.0.0"
|
||||
}
|
||||
@@ -9632,7 +9603,6 @@
|
||||
"integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^4.0.1",
|
||||
"is-core-module": "^2.5.0",
|
||||
@@ -9984,7 +9954,6 @@
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
@@ -10306,6 +10275,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -10494,6 +10464,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lilconfig": "^3.0.0",
|
||||
"yaml": "^2.3.4"
|
||||
@@ -10883,8 +10854,7 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz",
|
||||
"integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss-safe-parser": {
|
||||
"version": "6.0.0",
|
||||
@@ -11060,6 +11030,7 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -11363,7 +11334,6 @@
|
||||
"integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/normalize-package-data": "^2.4.0",
|
||||
"normalize-package-data": "^3.0.2",
|
||||
@@ -11383,7 +11353,6 @@
|
||||
"integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"find-up": "^5.0.0",
|
||||
"read-pkg": "^6.0.0",
|
||||
@@ -11402,7 +11371,6 @@
|
||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -11416,7 +11384,6 @@
|
||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -11459,7 +11426,6 @@
|
||||
"integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"indent-string": "^5.0.0",
|
||||
"strip-indent": "^4.0.0"
|
||||
@@ -12100,7 +12066,6 @@
|
||||
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
@@ -12177,7 +12142,6 @@
|
||||
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
@@ -12188,8 +12152,7 @@
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
|
||||
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
|
||||
"dev": true,
|
||||
"license": "CC-BY-3.0",
|
||||
"peer": true
|
||||
"license": "CC-BY-3.0"
|
||||
},
|
||||
"node_modules/spdx-expression-parse": {
|
||||
"version": "3.0.1",
|
||||
@@ -12197,7 +12160,6 @@
|
||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"spdx-exceptions": "^2.1.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
@@ -12208,8 +12170,7 @@
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
|
||||
"integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0",
|
||||
"peer": true
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
@@ -12303,7 +12264,6 @@
|
||||
"integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -12329,8 +12289,7 @@
|
||||
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
|
||||
"integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "0.4.4",
|
||||
@@ -12379,7 +12338,6 @@
|
||||
"integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/css-parser-algorithms": "^2.3.1",
|
||||
"@csstools/css-tokenizer": "^2.2.0",
|
||||
@@ -12462,7 +12420,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
},
|
||||
@@ -12476,7 +12433,6 @@
|
||||
"integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"flat-cache": "^3.2.0"
|
||||
},
|
||||
@@ -12489,8 +12445,7 @@
|
||||
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz",
|
||||
"integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylelint/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
@@ -12513,7 +12468,6 @@
|
||||
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -12654,7 +12608,6 @@
|
||||
"integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0",
|
||||
"supports-color": "^7.0.0"
|
||||
@@ -12684,6 +12637,7 @@
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz",
|
||||
"integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -12781,21 +12735,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -13025,8 +12964,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
|
||||
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.3.2",
|
||||
@@ -13077,7 +13015,6 @@
|
||||
"integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.1",
|
||||
"lodash.truncate": "^4.4.2",
|
||||
@@ -13105,6 +13042,7 @@
|
||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -13357,6 +13295,7 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13419,7 +13358,6 @@
|
||||
"integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13537,6 +13475,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -13773,7 +13712,6 @@
|
||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
@@ -13827,6 +13765,7 @@
|
||||
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
@@ -14396,9 +14335,9 @@
|
||||
"integrity": "sha512-uHNL8F72/Tf96xF3hOHnPDjkEyqXw7fNjcPJiUhth9sTQkcwUIoJMOdwm8/cs+j9kKVRJ4tgNYMHEBLylazp6g=="
|
||||
},
|
||||
"node_modules/windmill-utils-internal": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-utils-internal/-/windmill-utils-internal-1.7.0.tgz",
|
||||
"integrity": "sha512-K5kAiJKhavfGatmicbJyqT+KFspzFZK5Ou14pHj4Xg6Q2Z1a4ZgziBdrRNsaeAuBhB62yXWZA0OXDzg936Ymmw==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/windmill-utils-internal/-/windmill-utils-internal-1.8.2.tgz",
|
||||
"integrity": "sha512-Otdn4iCE0QiOtMKPdHX5I89oECfZa3vmO2jdLdSETf7MBSv/gw9gvQm3oUQ29ymIHXLeuhQk/ebPD+WYolQJEA==",
|
||||
"license": "Apache 2.0"
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
@@ -14534,7 +14473,6 @@
|
||||
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"signal-exit": "^4.0.1"
|
||||
@@ -14730,7 +14668,6 @@
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -14750,6 +14687,7 @@
|
||||
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
|
||||
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.99"
|
||||
},
|
||||
@@ -14785,6 +14723,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.750.0",
|
||||
"version": "1.751.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
|
||||
@@ -169,7 +169,7 @@
|
||||
"windmill-parser-wasm-wac": "1.668.6",
|
||||
"windmill-parser-wasm-yaml": "1.593.0",
|
||||
"windmill-sql-datatype-parser-wasm": "1.512.0",
|
||||
"windmill-utils-internal": "^1.7.0",
|
||||
"windmill-utils-internal": "1.8.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.2",
|
||||
"y-monaco": "^0.1.4",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
|
||||
"version": "062d11c",
|
||||
"sha256": "355bf3acfb935080e4a4fa23d0509e03e4bc01aeed622db110663d7ec6c1c438"
|
||||
"version": "f8cecf9",
|
||||
"sha256": "2a79b838e21e06abe06119263872afbe83c4a13f7aa72c722af3e34b819f0ce3"
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@
|
||||
deploymentStatus[statusKey] = { status: 'deployed' }
|
||||
} else {
|
||||
deploymentStatus[statusKey] = { status: 'failed', error: result.error }
|
||||
sendUserToast(`Failed to deploy ${statusKey}: ${result.error}`)
|
||||
sendUserToast(`Failed to deploy ${statusKey}: ${result.error}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +696,10 @@
|
||||
if (!aIsFolder && bIsFolder) return 1
|
||||
return 0
|
||||
})
|
||||
const to = mergeIntoParent ? parent : current
|
||||
let anyFailed = false
|
||||
// Datatables whose migrations deployed cleanly — candidates for a run prompt.
|
||||
const deployedMigrationDatatables = new Set<string>()
|
||||
for (const itemKey of sortedItems) {
|
||||
const deployable = deployableItems.find((d) => d.key === itemKey)
|
||||
|
||||
@@ -705,16 +708,19 @@
|
||||
continue
|
||||
}
|
||||
|
||||
const to = mergeIntoParent ? parent : current
|
||||
const from = mergeIntoParent ? current : parent
|
||||
await deploy(deployable.kind, deployable.path, to, from, itemKey)
|
||||
if (deploymentStatus[itemKey]?.status === 'failed') {
|
||||
anyFailed = true
|
||||
} else if (deployable.kind === 'datatable_migration') {
|
||||
deployedMigrationDatatables.add(deployable.path.split('/')[0])
|
||||
}
|
||||
}
|
||||
deploying = false
|
||||
deselectAll()
|
||||
|
||||
await maybePromptRunMigrations(deployedMigrationDatatables, to)
|
||||
|
||||
// If every selected item deployed cleanly and the direction was
|
||||
// merge-into-parent, resolve any open deployment request for this fork.
|
||||
if (!anyFailed && mergeIntoParent) {
|
||||
@@ -750,6 +756,51 @@
|
||||
onChanged?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* After a deploy, offer to run the migrations of every cloned datatable that
|
||||
* received one. `forked_from` is set on the fork's datatable config only when
|
||||
* the datatable was cloned into a separate database — shared-DB datatables
|
||||
* have already had the schema change applied and must not be re-run.
|
||||
*/
|
||||
async function maybePromptRunMigrations(
|
||||
deployedMigrationDatatables: Set<string>,
|
||||
runTargetWorkspace: string
|
||||
) {
|
||||
if (deployedMigrationDatatables.size === 0) return
|
||||
try {
|
||||
const forkSettings = await WorkspaceService.getPublicSettings({
|
||||
workspace: currentWorkspaceId
|
||||
})
|
||||
const datatables = forkSettings.datatable?.datatables ?? {}
|
||||
const cloned = [...deployedMigrationDatatables].filter(
|
||||
(dt) => datatables[dt]?.forked_from != null
|
||||
)
|
||||
if (cloned.length === 0) return
|
||||
runMigrationsDatatables = cloned.sort()
|
||||
runMigrationsTargetWorkspace = runTargetWorkspace
|
||||
runMigrationsModalOpen = true
|
||||
} catch (e) {
|
||||
console.error('Failed to determine cloned datatables for migration run prompt', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function runDeployedMigrations() {
|
||||
runMigrationsModalOpen = false
|
||||
for (const dt of runMigrationsDatatables) {
|
||||
try {
|
||||
const res = await WorkspaceService.runDatatableMigrations({
|
||||
workspace: runMigrationsTargetWorkspace,
|
||||
datatableName: dt
|
||||
})
|
||||
sendUserToast(
|
||||
`Ran ${res.applied.length} migration${res.applied.length !== 1 ? 's' : ''} on ${dt}`
|
||||
)
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Failed to run migrations on ${dt}: ${e.body ?? e.message ?? e}`, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleKey(key: string) {
|
||||
if (selectedItems.includes(key)) {
|
||||
selectedItems = selectedItems.filter((i) => i !== key)
|
||||
@@ -930,6 +981,17 @@
|
||||
let deploymentRequestPanel: DeploymentRequestPanel | undefined = $state(undefined)
|
||||
let hasOpenDeploymentRequest = $state(false)
|
||||
|
||||
// After deploying datatable migrations to a cloned (separate-DB) datatable, we
|
||||
// offer to run them in the target workspace. Shared-DB datatables are skipped:
|
||||
// the schema change is already physically applied, so re-running is redundant.
|
||||
let runMigrationsModalOpen = $state(false)
|
||||
let runMigrationsDatatables = $state<string[]>([])
|
||||
let runMigrationsTargetWorkspace = $state('')
|
||||
let runMigrationsTargetWorkspaceName = $derived(
|
||||
$userWorkspaces.find((w) => w.id == runMigrationsTargetWorkspace)?.name ??
|
||||
runMigrationsTargetWorkspace
|
||||
)
|
||||
|
||||
/** Display labels for trigger/schedule kinds in the merge UI. */
|
||||
const KIND_DISPLAY_NAMES: Record<string, string> = {
|
||||
schedule: 'Schedule',
|
||||
@@ -942,7 +1004,8 @@
|
||||
sqs_trigger: 'SQS trigger',
|
||||
gcp_trigger: 'GCP trigger',
|
||||
azure_trigger: 'Azure trigger',
|
||||
email_trigger: 'Email trigger'
|
||||
email_trigger: 'Email trigger',
|
||||
datatable_migration: 'Data table migration'
|
||||
}
|
||||
|
||||
// Human label for a diff kind, lowercased for inline use in the hidden-items
|
||||
@@ -1453,9 +1516,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-tertiary p-4 rounded-md border">
|
||||
<DatatableSchemaDiff {currentWorkspaceId} {parentWorkspaceId} />
|
||||
</div>
|
||||
<DatatableSchemaDiff {currentWorkspaceId} {parentWorkspaceId} />
|
||||
|
||||
{#if pinnedItems.length > 0}
|
||||
<div class="bg-surface-tertiary p-4 rounded-md border flex flex-col gap-2">
|
||||
@@ -1536,6 +1597,26 @@
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
open={runMigrationsModalOpen}
|
||||
title="Run datatable migrations?"
|
||||
confirmationText="Run migrations"
|
||||
onConfirmed={runDeployedMigrations}
|
||||
onCanceled={() => (runMigrationsModalOpen = false)}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
Run the deployed migrations in <b>{runMigrationsTargetWorkspaceName}</b> now? These data tables
|
||||
use a separate database, so the schema changes won't apply until the migrations are run.
|
||||
</p>
|
||||
<ul class="list-disc pl-5 text-sm font-mono text-secondary">
|
||||
{#each runMigrationsDatatables as dt (dt)}
|
||||
<li>{dt}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
open={!!createConfirm}
|
||||
title="Create in {createConfirm?.onCurrent ? parentWorkspaceId : currentWorkspaceId}?"
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
refresh?.()
|
||||
sendUserToast(`Schema '${schemaKey}' deleted successfully`)
|
||||
} catch (e) {
|
||||
let msg: string | undefined = (e as Error).message
|
||||
let msg: string | undefined = (e as any).body ?? (e as Error).message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined
|
||||
sendUserToast(msg ?? 'Action failed!', true)
|
||||
}
|
||||
@@ -436,7 +436,7 @@
|
||||
refresh?.()
|
||||
sendUserToast(`Table '${tableKey}' deleted successfully`)
|
||||
} catch (e) {
|
||||
let msg: string | undefined = (e as Error).message
|
||||
let msg: string | undefined = (e as any).body ?? (e as Error).message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined
|
||||
sendUserToast(msg ?? 'Action failed!', true)
|
||||
}
|
||||
@@ -502,7 +502,7 @@
|
||||
refresh?.()
|
||||
sendUserToast(`Table '${tableKey}' deleted successfully`)
|
||||
} catch (e) {
|
||||
let msg: string | undefined = (e as Error).message
|
||||
let msg: string | undefined = (e as any).body ?? (e as Error).message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined
|
||||
sendUserToast(msg ?? 'Action failed!', true)
|
||||
}
|
||||
@@ -605,7 +605,9 @@
|
||||
onConfirm={async ({ values }) => {
|
||||
if (dbTableEditorState.alterTableKey && dbTableEditorAlterTableData.current) {
|
||||
let diff = diffTableEditorValues(dbTableEditorAlterTableData.current, values)
|
||||
await dbSchemaOps.onAlter({ schema: selected.schemaKey, values: diff })
|
||||
// Reverse diff (new → old) so the migration's down undoes the alter.
|
||||
let reverse = diffTableEditorValues(values, dbTableEditorAlterTableData.current)
|
||||
await dbSchemaOps.onAlter({ schema: selected.schemaKey, values: diff, reverse })
|
||||
} else {
|
||||
await dbSchemaOps.onCreate({ values, schema: selected.schemaKey })
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
import type { SelectedTable } from './DBManager.svelte'
|
||||
import { getDbFeatures } from './apps/components/display/dbtable/dbFeatures'
|
||||
import { resource } from 'runed'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { createAsyncConfirmationModal } from './common/confirmationModal/asyncConfirmationModal.svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { outOfOrderRunMessage } from './workspaceSettings/datatableMigrationUtils'
|
||||
|
||||
interface Props {
|
||||
input?: DbInput
|
||||
@@ -52,6 +56,8 @@
|
||||
|
||||
let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[getDbSchemasPath(input)])
|
||||
|
||||
const outOfOrderModal = createAsyncConfirmationModal()
|
||||
|
||||
function getDbSchemasPath(input: DbInput): string {
|
||||
switch (input.type) {
|
||||
case 'database':
|
||||
@@ -163,7 +169,13 @@
|
||||
})}
|
||||
dbSchemaOps={dbSchemaOpsWithPreviewScripts({
|
||||
input: _input,
|
||||
workspace: $workspaceStore
|
||||
workspace: $workspaceStore,
|
||||
confirmRunOutOfOrder: (pending) =>
|
||||
outOfOrderModal.ask({
|
||||
title: 'Run migration out of order',
|
||||
confirmationText: 'Run anyway',
|
||||
children: outOfOrderRunMessage(pending)
|
||||
})
|
||||
})}
|
||||
initialTableKey={input.specificTable}
|
||||
initialSchemaKey={input.specificSchema}
|
||||
@@ -192,6 +204,7 @@
|
||||
onData={(data) => {
|
||||
replResultData = data
|
||||
}}
|
||||
onSchemaChange={() => refresh()}
|
||||
placeholderTableName={sortArray(
|
||||
Object.keys(
|
||||
dbSchema?.schema[
|
||||
@@ -214,3 +227,9 @@
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{/if}
|
||||
|
||||
<Portal>
|
||||
<!-- Stacks above the DB table editor's own preview confirmation (z-[9999]),
|
||||
which is still open when applyDdl asks for out-of-order confirmation. -->
|
||||
<ConfirmationModal {...outOfOrderModal.props} zIndexClass="z-[10000]" />
|
||||
</Portal>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
Upload
|
||||
} from 'lucide-svelte'
|
||||
import DBManagerContent from './DBManagerContent.svelte'
|
||||
import DataTableMigrationsButton from './workspaceSettings/DataTableMigrationsButton.svelte'
|
||||
import { resource } from 'runed'
|
||||
import { untrack } from 'svelte'
|
||||
import type { DbManagerUriState } from './dbManagerDrawerModel.svelte'
|
||||
@@ -105,6 +106,11 @@
|
||||
return toSourceIdentifier(input.resourcePath)
|
||||
}
|
||||
|
||||
function refreshManager() {
|
||||
dbManagerContent?.refresh()
|
||||
dbManagerContent?.dbManager()?.dbTable()?.refresh()
|
||||
}
|
||||
|
||||
async function handleExportSchema() {
|
||||
const source = currentSourceIdentifier()
|
||||
if (!source || !$workspaceStore) return
|
||||
@@ -201,6 +207,13 @@
|
||||
{/key}
|
||||
{/if}
|
||||
{#snippet actions()}
|
||||
{#if uriState.isDatatableInput && uriState.selectedDatatable && $workspaceStore}
|
||||
<DataTableMigrationsButton
|
||||
workspace={$workspaceStore}
|
||||
datatable={uriState.selectedDatatable}
|
||||
onSchemaChanged={refreshManager}
|
||||
/>
|
||||
{/if}
|
||||
{#if enableImportExport}
|
||||
<Button startIcon={{ icon: Download }} onClick={handleExportSchema}>Export</Button>
|
||||
<Button startIcon={{ icon: Upload }} onClick={() => (importDrawerOpen = true)}>
|
||||
@@ -209,16 +222,13 @@
|
||||
{/if}
|
||||
<Button
|
||||
loading={dbManagerContent?.isLoading() ?? false}
|
||||
on:click={() => {
|
||||
dbManagerContent?.refresh()
|
||||
dbManagerContent?.dbManager()?.dbTable()?.refresh()
|
||||
}}
|
||||
on:click={refreshManager}
|
||||
startIcon={{ icon: RefreshCcw }}
|
||||
iconOnly
|
||||
title="Refresh"
|
||||
size="xs"
|
||||
color="light"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
/>
|
||||
|
||||
<Button
|
||||
on:click={() => (expand = !expand)}
|
||||
@@ -234,7 +244,9 @@
|
||||
<DrawerContent title="Export Schemas" on:close={() => (exportDrawerOpen = false)}>
|
||||
{#if exportResult}
|
||||
<div class="flex flex-col gap-2 h-full relative">
|
||||
<pre class="overflow-auto text-xs bg-surface-secondary p-4 rounded flex-1">{exportResult}</pre>
|
||||
<pre class="overflow-auto text-xs bg-surface-secondary p-4 rounded flex-1"
|
||||
>{exportResult}</pre
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { MigrationRunCancelled } from './dbOps'
|
||||
import { getFlatTableNamesFromSchema, type DBSchema } from '$lib/stores'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
@@ -460,9 +461,13 @@
|
||||
askingForConfirmation && (askingForConfirmation.loading = true)
|
||||
await onConfirm({ values })
|
||||
} catch (e) {
|
||||
let msg: string | undefined = (e as Error)?.message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : 'An error occurred'
|
||||
sendUserToast(msg, true)
|
||||
// User declined the out-of-order run warning: silent cancel,
|
||||
// leave the editor open so they can adjust or run earlier first.
|
||||
if (!(e instanceof MigrationRunCancelled)) {
|
||||
let msg: string | undefined = (e as any)?.body ?? (e as Error)?.message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : 'An error occurred'
|
||||
sendUserToast(msg, true)
|
||||
}
|
||||
}
|
||||
askingForConfirmation = undefined
|
||||
},
|
||||
@@ -472,7 +477,7 @@
|
||||
...(preview && { codeContent: preview.sql, alert: preview.alert })
|
||||
}
|
||||
} catch (e) {
|
||||
let msg: string | undefined = (e as Error)?.message
|
||||
let msg: string | undefined = (e as any)?.body ?? (e as Error)?.message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : 'An error occurred'
|
||||
sendUserToast(msg, true)
|
||||
} finally {
|
||||
|
||||
@@ -196,10 +196,17 @@
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { userWorkspaces } from '$lib/stores'
|
||||
import { runScriptAndPollResult } from '$lib/components/jobs/utils'
|
||||
import YAML from 'yaml'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { createAsyncConfirmationModal } from './common/confirmationModal/asyncConfirmationModal.svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import {
|
||||
pendingMigrations,
|
||||
outOfOrderRunMessage
|
||||
} from './workspaceSettings/datatableMigrationUtils'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
|
||||
|
||||
@@ -213,6 +220,11 @@
|
||||
let loading = $state(true)
|
||||
let error: string | undefined = $state(undefined)
|
||||
let diffs: DatatableDiff[] = $state([])
|
||||
// Number of forked datatables this schema-diff section applies to: those that
|
||||
// have NOT opted in to the migrations feature. When a datatable enables
|
||||
// migrations, its changes flow through the normal item diff instead, so it is
|
||||
// excluded here and the whole section hides once none remain.
|
||||
let applicableCount = $state(0)
|
||||
let expandedDatatables: Set<string> = $state(new Set())
|
||||
|
||||
// Drawer state
|
||||
@@ -223,6 +235,7 @@
|
||||
let migrationSql = $state('')
|
||||
let migrationRunning = $state(false)
|
||||
let confirmDeployOpen = $state(false)
|
||||
const outOfOrderModal = createAsyncConfirmationModal()
|
||||
|
||||
async function loadDiffs() {
|
||||
loading = true
|
||||
@@ -233,7 +246,10 @@
|
||||
workspace: currentWorkspaceId
|
||||
})
|
||||
const datatables = forkSettings.datatable?.datatables ?? {}
|
||||
const forkedEntries = Object.entries(datatables).filter(([_, dt]) => dt.forked_from != null)
|
||||
const forkedEntries = Object.entries(datatables).filter(
|
||||
([_, dt]) => dt.forked_from != null && dt.migrations_enabled !== true
|
||||
)
|
||||
applicableCount = forkedEntries.length
|
||||
if (forkedEntries.length === 0) {
|
||||
loading = false
|
||||
return
|
||||
@@ -343,14 +359,68 @@
|
||||
const dtName = drawerDiff.datatableName
|
||||
|
||||
try {
|
||||
await runScriptAndPollResult({
|
||||
// If the target data table opted in to migrations, record this merge as a
|
||||
// tracked migration (named after the fork) and run it, instead of applying
|
||||
// raw SQL that would bypass the target's migration history.
|
||||
// Don't swallow a status-check failure by defaulting to raw apply: that
|
||||
// would apply the DDL untracked (schema drift) — exactly what this feature
|
||||
// prevents. Let the error propagate (fail closed, handled by the outer
|
||||
// catch); only fall back to raw apply when the API explicitly returns
|
||||
// enabled === false.
|
||||
const status = await WorkspaceService.getDatatableMigrationsStatus({
|
||||
workspace: targetWorkspace,
|
||||
requestBody: {
|
||||
args: { database: `datatable://${dtName}` },
|
||||
language: 'postgresql',
|
||||
content: migrationSql
|
||||
}
|
||||
datatableName: dtName
|
||||
})
|
||||
|
||||
if (status.enabled) {
|
||||
// The merge migration gets the highest timestamp, so any still-pending
|
||||
// migration on the target is earlier: running only the merge applies it
|
||||
// out of order. Warn like the row-level Run action does.
|
||||
const pending = pendingMigrations(status.migrations)
|
||||
if (pending.length > 0) {
|
||||
const confirmed = await outOfOrderModal.ask({
|
||||
title: 'Run migration out of order',
|
||||
confirmationText: 'Run anyway',
|
||||
children: outOfOrderRunMessage(pending.length)
|
||||
})
|
||||
if (!confirmed) {
|
||||
migrationRunning = false
|
||||
return
|
||||
}
|
||||
}
|
||||
const forkName =
|
||||
$userWorkspaces.find((w) => w.id === currentWorkspaceId)?.name || currentWorkspaceId
|
||||
const migName = `merge_${forkName}`.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
||||
const created = await WorkspaceService.createDatatableMigration({
|
||||
workspace: targetWorkspace,
|
||||
datatableName: dtName,
|
||||
requestBody: { name: migName, code_up: migrationSql }
|
||||
})
|
||||
try {
|
||||
await WorkspaceService.runDatatableMigrations({
|
||||
workspace: targetWorkspace,
|
||||
datatableName: dtName,
|
||||
only: created.timestamp
|
||||
})
|
||||
} catch (runErr: any) {
|
||||
// Undo the insertion so the user can fix and retry from a clean state.
|
||||
await WorkspaceService.deleteDatatableMigration({
|
||||
workspace: targetWorkspace,
|
||||
datatableName: dtName,
|
||||
timestamp: created.timestamp
|
||||
}).catch(() => {})
|
||||
throw runErr
|
||||
}
|
||||
} else {
|
||||
await runScriptAndPollResult({
|
||||
workspace: targetWorkspace,
|
||||
requestBody: {
|
||||
args: { database: `datatable://${dtName}` },
|
||||
language: 'postgresql',
|
||||
content: migrationSql
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
sendUserToast(e?.body ?? e?.message ?? String(e), true)
|
||||
migrationRunning = false
|
||||
@@ -394,102 +464,107 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="text-sm font-semibold">Datatable schema changes</h3>
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary py-2">
|
||||
<Loader2 class="w-4 h-4 animate-spin" /> Loading datatable diffs...
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-xs text-red-500 py-2">Failed to load datatable diffs: {error}</div>
|
||||
{:else if diffs.length > 0}
|
||||
<div class="flex flex-col gap-2 mt-3 mb-1">
|
||||
{#each diffs as diff}
|
||||
<ResizeTransitionWrapper class="border rounded-md" innerClass="w-full" vertical>
|
||||
<button
|
||||
class="w-full flex items-center justify-between px-3 py-2 hover:bg-surface-hover"
|
||||
onclick={() => toggleExpanded(diff.datatableName)}
|
||||
>
|
||||
<span class="text-xs font-medium">{diff.datatableName}</span>
|
||||
<div class="flex items-center gap-2 text-2xs text-tertiary">
|
||||
{#if diff.aheadChanges.length > 0}
|
||||
<span class="text-blue-500">{diff.aheadChanges.length} ahead</span>
|
||||
{/if}
|
||||
{#if diff.behindChanges.length > 0}
|
||||
<span class="text-orange-500">{diff.behindChanges.length} behind</span>
|
||||
{/if}
|
||||
{#if expandedDatatables.has(diff.datatableName)}
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
{:else}
|
||||
<ChevronRight class="w-3 h-3" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{#if applicableCount > 0}
|
||||
<div class="bg-surface-tertiary p-4 rounded-md border">
|
||||
<h3 class="text-sm font-semibold">Datatable schema changes</h3>
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary py-2">
|
||||
<Loader2 class="w-4 h-4 animate-spin" /> Loading datatable diffs...
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-xs text-red-500 py-2">Failed to load datatable diffs: {error}</div>
|
||||
{:else if diffs.length > 0}
|
||||
<div class="flex flex-col gap-2 mt-3 mb-1">
|
||||
{#each diffs as diff}
|
||||
<ResizeTransitionWrapper class="border rounded-md" innerClass="w-full" vertical>
|
||||
<button
|
||||
class="w-full flex items-center justify-between px-3 py-2 hover:bg-surface-hover"
|
||||
onclick={() => toggleExpanded(diff.datatableName)}
|
||||
>
|
||||
<span class="text-xs font-medium">{diff.datatableName}</span>
|
||||
<div class="flex items-center gap-2 text-2xs text-tertiary">
|
||||
{#if diff.aheadChanges.length > 0}
|
||||
<span class="text-blue-500">{diff.aheadChanges.length} ahead</span>
|
||||
{/if}
|
||||
{#if diff.behindChanges.length > 0}
|
||||
<span class="text-orange-500">{diff.behindChanges.length} behind</span>
|
||||
{/if}
|
||||
{#if expandedDatatables.has(diff.datatableName)}
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
{:else}
|
||||
<ChevronRight class="w-3 h-3" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if expandedDatatables.has(diff.datatableName)}
|
||||
<div class="border-t divide-y">
|
||||
{#if diff.aheadChanges.length > 0}
|
||||
<div class="px-3 py-1.5">
|
||||
<div class="text-2xs font-semibold text-blue-500 mb-1">Fork changes (ahead)</div>
|
||||
{#each diff.aheadChanges as change}
|
||||
<div class="flex items-center gap-2 text-xs py-0.5">
|
||||
{#if change.kind === 'added'}
|
||||
<Plus class="w-3 h-3 text-green-500 shrink-0" />
|
||||
{:else if change.kind === 'removed'}
|
||||
<Minus class="w-3 h-3 text-red-500 shrink-0" />
|
||||
{:else}
|
||||
<Pencil class="w-3 h-3 text-yellow-500 shrink-0" />
|
||||
{/if}
|
||||
<span class="text-tertiary">{change.schemaName}.</span>
|
||||
<span class="font-medium">{change.tableName}</span>
|
||||
<span class="text-tertiary text-2xs grow">{operationSummary(change)}</span>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: Eye }}
|
||||
onclick={() => openReview(change, diff, 'ahead')}
|
||||
{#if expandedDatatables.has(diff.datatableName)}
|
||||
<div class="border-t divide-y">
|
||||
{#if diff.aheadChanges.length > 0}
|
||||
<div class="px-3 py-1.5">
|
||||
<div class="text-2xs font-semibold text-blue-500 mb-1">Fork changes (ahead)</div
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
{#each diff.aheadChanges as change}
|
||||
<div class="flex items-center gap-2 text-xs py-0.5">
|
||||
{#if change.kind === 'added'}
|
||||
<Plus class="w-3 h-3 text-green-500 shrink-0" />
|
||||
{:else if change.kind === 'removed'}
|
||||
<Minus class="w-3 h-3 text-red-500 shrink-0" />
|
||||
{:else}
|
||||
<Pencil class="w-3 h-3 text-yellow-500 shrink-0" />
|
||||
{/if}
|
||||
<span class="text-tertiary">{change.schemaName}.</span>
|
||||
<span class="font-medium">{change.tableName}</span>
|
||||
<span class="text-tertiary text-2xs grow">{operationSummary(change)}</span>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: Eye }}
|
||||
onclick={() => openReview(change, diff, 'ahead')}
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if diff.behindChanges.length > 0}
|
||||
<div class="px-3 py-1.5">
|
||||
<div class="text-2xs font-semibold text-orange-500 mb-1">
|
||||
Parent changes (behind)
|
||||
</div>
|
||||
{#each diff.behindChanges as change}
|
||||
<div class="flex items-center gap-2 text-xs py-0.5">
|
||||
{#if change.kind === 'added'}
|
||||
<Plus class="w-3 h-3 text-green-500 shrink-0" />
|
||||
{:else if change.kind === 'removed'}
|
||||
<Minus class="w-3 h-3 text-red-500 shrink-0" />
|
||||
{:else}
|
||||
<Pencil class="w-3 h-3 text-yellow-500 shrink-0" />
|
||||
{/if}
|
||||
<span class="text-tertiary">{change.schemaName}.</span>
|
||||
<span class="font-medium">{change.tableName}</span>
|
||||
<span class="text-tertiary text-2xs grow">{operationSummary(change)}</span>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: Eye }}
|
||||
onclick={() => openReview(change, diff, 'behind')}
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if diff.behindChanges.length > 0}
|
||||
<div class="px-3 py-1.5">
|
||||
<div class="text-2xs font-semibold text-orange-500 mb-1">
|
||||
Parent changes (behind)
|
||||
</div>
|
||||
{#each diff.behindChanges as change}
|
||||
<div class="flex items-center gap-2 text-xs py-0.5">
|
||||
{#if change.kind === 'added'}
|
||||
<Plus class="w-3 h-3 text-green-500 shrink-0" />
|
||||
{:else if change.kind === 'removed'}
|
||||
<Minus class="w-3 h-3 text-red-500 shrink-0" />
|
||||
{:else}
|
||||
<Pencil class="w-3 h-3 text-yellow-500 shrink-0" />
|
||||
{/if}
|
||||
<span class="text-tertiary">{change.schemaName}.</span>
|
||||
<span class="font-medium">{change.tableName}</span>
|
||||
<span class="text-tertiary text-2xs grow">{operationSummary(change)}</span>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: Eye }}
|
||||
onclick={() => openReview(change, diff, 'behind')}
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</ResizeTransitionWrapper>
|
||||
{/each}
|
||||
</ResizeTransitionWrapper>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs text-secondary"> No changes detected </span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs text-secondary"> No changes detected </span>
|
||||
{/if}
|
||||
|
||||
<Drawer bind:open={drawerOpen} size="900px">
|
||||
@@ -580,3 +655,7 @@
|
||||
>{migrationSql}</pre
|
||||
>
|
||||
</ConfirmationModal>
|
||||
|
||||
<Portal>
|
||||
<ConfirmationModal {...outOfOrderModal.props} />
|
||||
</Portal>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<script lang="ts">
|
||||
import { Button } from './common'
|
||||
import Modal2 from './common/modal/Modal2.svelte'
|
||||
import NewDataTableMigrationModal from './workspaceSettings/NewDataTableMigrationModal.svelte'
|
||||
import DataTableMigrationsButton from './workspaceSettings/DataTableMigrationsButton.svelte'
|
||||
import { splitSqlStatements, isDdlStatement } from './sqlDdl'
|
||||
import { CornerDownLeft } from 'lucide-svelte'
|
||||
|
||||
let { workspace, datatable }: { workspace: string; datatable: string } = $props()
|
||||
|
||||
type Choice = 'run' | 'migrate' | 'cancel'
|
||||
|
||||
let promptStatement = $state<string | undefined>(undefined)
|
||||
let promptOpen = $state(false)
|
||||
let resolvePrompt: ((choice: Choice) => void) | undefined = undefined
|
||||
let resolveMigrationClosed: ((created: boolean) => void) | undefined = undefined
|
||||
// Set when a migration is created *and run* during a guard() call, so the
|
||||
// caller can refresh the schema afterwards.
|
||||
let migrationRan = false
|
||||
let newMigrationModal = $state<NewDataTableMigrationModal | undefined>(undefined)
|
||||
// Triggerless Migrations modal, opened programmatically from the "See migration"
|
||||
// toast action after a migration is created here.
|
||||
let migrationsModal = $state<DataTableMigrationsButton | undefined>(undefined)
|
||||
|
||||
function finishPrompt(choice: Choice) {
|
||||
const r = resolvePrompt
|
||||
resolvePrompt = undefined
|
||||
promptOpen = false
|
||||
promptStatement = undefined
|
||||
r?.(choice)
|
||||
}
|
||||
|
||||
// Closing the modal (X / escape) while a prompt is pending counts as cancel.
|
||||
$effect(() => {
|
||||
if (!promptOpen && resolvePrompt) {
|
||||
finishPrompt('cancel')
|
||||
}
|
||||
})
|
||||
|
||||
// Enter confirms the primary action ("Create a migration"). Ignore modified
|
||||
// Enter so editor shortcuts (Cmd/Ctrl+Enter) keep working underneath.
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (!promptOpen || event.metaKey || event.ctrlKey || event.altKey) return
|
||||
if (event.key === 'Enter') {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
finishPrompt('migrate')
|
||||
}
|
||||
}
|
||||
|
||||
function promptDdl(statement: string): Promise<Choice> {
|
||||
return new Promise((resolve) => {
|
||||
resolvePrompt = resolve
|
||||
promptStatement = statement
|
||||
promptOpen = true
|
||||
})
|
||||
}
|
||||
|
||||
function handleMigrationClosed(result: { created: boolean; ran: boolean }) {
|
||||
if (result.ran) migrationRan = true
|
||||
const r = resolveMigrationClosed
|
||||
resolveMigrationClosed = undefined
|
||||
r?.(result.created)
|
||||
}
|
||||
|
||||
// Open the prefilled new-migration modal. Resolves with whether a migration
|
||||
// was actually created (false if the user cancelled / closed it).
|
||||
function openMigrationModal(statement: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
resolveMigrationClosed = (created: boolean) => resolve(created)
|
||||
newMigrationModal?.open({ codeUp: statement })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect `code` for DDL statements. For each one, prompt the user to run it
|
||||
* anyway or turn it into a migration (prompts shown one at a time). Returns
|
||||
* whether to proceed and the code to run (with migrated statements stripped).
|
||||
*/
|
||||
export async function guard(
|
||||
code: string
|
||||
): Promise<{ proceed: boolean; code: string; ranMigration: boolean }> {
|
||||
migrationRan = false
|
||||
const statements = splitSqlStatements(code)
|
||||
if (!statements.some((s) => isDdlStatement(s))) {
|
||||
return { proceed: true, code, ranMigration: false }
|
||||
}
|
||||
|
||||
const kept: string[] = []
|
||||
for (const statement of statements) {
|
||||
if (!isDdlStatement(statement)) {
|
||||
kept.push(statement)
|
||||
continue
|
||||
}
|
||||
// Re-prompt for this statement until the user makes a terminal choice;
|
||||
// cancelling the migration modal returns to the prompt with the DDL intact.
|
||||
for (;;) {
|
||||
const choice = await promptDdl(statement)
|
||||
if (choice === 'cancel') {
|
||||
return { proceed: false, code, ranMigration: migrationRan }
|
||||
}
|
||||
if (choice === 'run') {
|
||||
kept.push(statement)
|
||||
break
|
||||
}
|
||||
// migrate: only strip the statement once a migration is actually
|
||||
// created; if the modal was cancelled, loop back to the prompt.
|
||||
const created = await openMigrationModal(statement)
|
||||
if (created) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { proceed: true, code: kept.join(';\n'), ranMigration: migrationRan }
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
|
||||
<Modal2
|
||||
title="Schema change detected"
|
||||
fixedWidth="md"
|
||||
fixedHeight="adaptive"
|
||||
bind:isOpen={promptOpen}
|
||||
closeOnOutsideClick={false}
|
||||
>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<p class="text-sm text-secondary">
|
||||
This looks like a schema-changing (DDL) statement. Schema changes are best tracked as
|
||||
migrations rather than run ad-hoc. Create a migration for it instead?
|
||||
</p>
|
||||
<pre
|
||||
class="text-xs whitespace-pre-wrap font-mono bg-surface-secondary rounded p-3 max-h-48 overflow-auto"
|
||||
>{promptStatement ?? ''}</pre
|
||||
>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<Button variant="default" size="sm" on:click={() => finishPrompt('run')}>Run anyway</Button>
|
||||
<Button
|
||||
variant="accent"
|
||||
size="sm"
|
||||
shortCut={{ Icon: CornerDownLeft, withoutModifier: true }}
|
||||
on:click={() => finishPrompt('migrate')}
|
||||
>
|
||||
Create a migration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal2>
|
||||
|
||||
<NewDataTableMigrationModal
|
||||
bind:this={newMigrationModal}
|
||||
{workspace}
|
||||
{datatable}
|
||||
onClose={handleMigrationClosed}
|
||||
onSeeMigration={(m) => migrationsModal?.openMigration(m.timestamp)}
|
||||
/>
|
||||
|
||||
<DataTableMigrationsButton bind:this={migrationsModal} hideTrigger {workspace} {datatable} />
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore, userWorkspaces, usersWorkspaceStore } from '$lib/stores'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -9,12 +9,14 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { base } from '$lib/base'
|
||||
import { findCanonicalDevWorkspace } from '$lib/utils/workspaceHierarchy'
|
||||
import { devBadgeText, devLabelKey, devLabelNoun } from '$lib/utils/devWorkspaceLabel'
|
||||
import { loadProtectionRules } from '$lib/workspaceProtectionRules.svelte'
|
||||
import { GitFork, ExternalLink } from 'lucide-svelte'
|
||||
import { resource } from 'runed'
|
||||
|
||||
let currentWs = $derived($userWorkspaces.find((w) => w.id === $workspaceStore))
|
||||
let isDev = $derived(currentWs?.is_dev_workspace ?? false)
|
||||
let currentLabel = $derived(devLabelKey(currentWs?.dev_workspace_label))
|
||||
let parentId = $derived(currentWs?.parent_workspace_id ?? undefined)
|
||||
let canonicalDev = $derived(findCanonicalDevWorkspace($workspaceStore, $userWorkspaces))
|
||||
|
||||
@@ -29,12 +31,18 @@
|
||||
// else the server result (pairing + detach still available to a prod admin).
|
||||
let pairedDev = $derived(
|
||||
canonicalDev
|
||||
? { id: canonicalDev.id, name: canonicalDev.name, isMember: true }
|
||||
? {
|
||||
id: canonicalDev.id,
|
||||
name: canonicalDev.name,
|
||||
isMember: true,
|
||||
label: canonicalDev.dev_workspace_label
|
||||
}
|
||||
: devWorkspaceResource.current
|
||||
? {
|
||||
id: devWorkspaceResource.current.id,
|
||||
name: devWorkspaceResource.current.name,
|
||||
isMember: false
|
||||
isMember: false,
|
||||
label: devWorkspaceResource.current.dev_workspace_label
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
@@ -42,7 +50,10 @@
|
||||
let selectedDevId = $state<string | undefined>(undefined)
|
||||
let lockProdDeploy = $state(true)
|
||||
let lockProdForking = $state(true)
|
||||
// Cosmetic display label chosen when attaching an existing workspace as dev.
|
||||
let attachLabel = $state<'dev' | 'staging'>('dev')
|
||||
let busy = $state(false)
|
||||
let labelBusy = $state(false)
|
||||
|
||||
// A standalone root workspace, or an existing fork of this prod (same family), can be attached.
|
||||
// A fork parented to a different workspace can't (the backend rejects a parent that isn't this
|
||||
@@ -82,10 +93,11 @@
|
||||
requestBody: {
|
||||
dev_workspace_id: selectedDevId,
|
||||
lock_prod_deploy: lockProdDeploy,
|
||||
lock_prod_forking: lockProdForking
|
||||
lock_prod_forking: lockProdForking,
|
||||
dev_workspace_label: attachLabel
|
||||
}
|
||||
})
|
||||
sendUserToast(`Attached ${selectedDevId} as dev workspace`)
|
||||
sendUserToast(`Attached ${selectedDevId} as ${attachLabel} workspace`)
|
||||
selectedDevId = undefined
|
||||
await refresh()
|
||||
} catch (e: any) {
|
||||
@@ -95,6 +107,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function setLabel(label: 'dev' | 'staging') {
|
||||
if (!$workspaceStore || label === devLabelKey(currentWs?.dev_workspace_label)) return
|
||||
labelBusy = true
|
||||
try {
|
||||
await WorkspaceService.setDevWorkspaceLabel({
|
||||
workspace: $workspaceStore,
|
||||
requestBody: { dev_workspace_label: label }
|
||||
})
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Failed to update display label: ${e?.body ?? e}`, true)
|
||||
} finally {
|
||||
labelBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
async function detach(devId: string) {
|
||||
if (!$workspaceStore) return
|
||||
busy = true
|
||||
@@ -116,9 +144,20 @@
|
||||
{#if isDev && parentId}
|
||||
<div class="flex flex-col gap-3 max-w-2xl">
|
||||
<p class="text-sm">
|
||||
This is a <b>dev workspace</b> paired with root workspace <b>{parentId}</b>. Promote changes
|
||||
from the home page banner or the Compare & Deploy page.
|
||||
This is a <b>{devLabelNoun(currentWs?.dev_workspace_label)}</b> paired with root workspace
|
||||
<b>{parentId}</b>. Promote changes from the home page banner or the Compare & Deploy page.
|
||||
</p>
|
||||
<div class="text-2xs text-secondary">
|
||||
Cosmetic label: <Badge color="indigo" small>{devBadgeText(currentLabel)}</Badge>
|
||||
<button
|
||||
type="button"
|
||||
disabled={labelBusy}
|
||||
class="text-secondary hover:text-primary hover:underline disabled:opacity-50"
|
||||
onclick={() => setLabel(currentLabel === 'staging' ? 'dev' : 'staging')}
|
||||
>
|
||||
Change to {currentLabel === 'staging' ? 'dev' : 'staging'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -132,8 +171,8 @@
|
||||
{:else if pairedDev}
|
||||
<div class="flex flex-col gap-3 max-w-2xl">
|
||||
<p class="text-sm">
|
||||
This workspace's dev workspace is <b>{pairedDev.name}</b> ({pairedDev.id}). Edits to this
|
||||
workspace are redirected there.
|
||||
This workspace's {devLabelNoun(pairedDev.label)} is <b>{pairedDev.name}</b> ({pairedDev.id}).
|
||||
Edits to this workspace are redirected there.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
{#if pairedDev.isMember}
|
||||
@@ -142,7 +181,7 @@
|
||||
startIcon={{ icon: GitFork }}
|
||||
onclick={() => switchWorkspace(pairedDev.id)}
|
||||
>
|
||||
Go to dev workspace
|
||||
Go to {devLabelNoun(pairedDev.label)}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button color="red" disabled={busy} onclick={() => detach(pairedDev.id)}>Detach</Button>
|
||||
@@ -168,6 +207,16 @@
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="text-2xs text-secondary">
|
||||
Cosmetic label: <Badge color="indigo" small>{devBadgeText(attachLabel)}</Badge>
|
||||
<button
|
||||
type="button"
|
||||
class="text-secondary hover:text-primary hover:underline"
|
||||
onclick={() => (attachLabel = attachLabel === 'staging' ? 'dev' : 'staging')}
|
||||
>
|
||||
Change to {attachLabel === 'staging' ? 'dev' : 'staging'}
|
||||
</button>
|
||||
</div>
|
||||
<Toggle
|
||||
bind:checked={lockProdDeploy}
|
||||
options={{
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
import { editorFontSize } from '$lib/editorFontSize.svelte'
|
||||
import { createHash as randomHash } from '$lib/editorLangUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
|
||||
import {
|
||||
type Preview,
|
||||
ResourceService,
|
||||
@@ -222,6 +223,35 @@
|
||||
|
||||
let lang = $state(scriptLangToEditorLang(untrack(() => scriptLang)))
|
||||
|
||||
// On a postgres script targeting a datatable, DDL statements are intercepted
|
||||
// on run (cmd+enter) and offered as migrations instead.
|
||||
let datatableForMigrations = $derived(
|
||||
scriptLang === 'postgresql' &&
|
||||
typeof args?.database === 'string' &&
|
||||
args.database.startsWith('datatable://')
|
||||
? args.database.slice('datatable://'.length).split('/')[0]
|
||||
: undefined
|
||||
)
|
||||
let ddlGuard = $state<DdlMigrationGuard | undefined>(undefined)
|
||||
|
||||
// Run the DDL migration guard against the current code. Returns false when the
|
||||
// user cancels (the run must be aborted); may rewrite the code (migrated
|
||||
// statements stripped). Exported so run paths that bypass the Monaco
|
||||
// Cmd+Enter binding (e.g. the Test button) can guard too.
|
||||
export async function guardDdlBeforeRun(): Promise<boolean> {
|
||||
if (datatableForMigrations && ddlGuard) {
|
||||
const res = await ddlGuard.guard(getCode())
|
||||
if (!res.proceed) return false
|
||||
if (res.code !== getCode()) setCode(res.code)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function runCmdEnterWithDdlGuard() {
|
||||
if (!(await guardDdlBeforeRun())) return
|
||||
cmdEnterAction?.()
|
||||
}
|
||||
|
||||
let filePath = $state(computePath(untrack(() => path)))
|
||||
|
||||
let initialPath: string | undefined = $state(untrack(() => path))
|
||||
@@ -1639,7 +1669,8 @@
|
||||
|
||||
editor?.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, function () {
|
||||
updateCode()
|
||||
shouldBindKey && cmdEnterAction && cmdEnterAction()
|
||||
if (!shouldBindKey || !cmdEnterAction) return
|
||||
void runCmdEnterWithDdlGuard()
|
||||
})
|
||||
|
||||
editor?.addCommand(KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit7, function () {
|
||||
@@ -2209,6 +2240,13 @@
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
<EditorTheme />
|
||||
{#if datatableForMigrations && $workspaceStore}
|
||||
<DdlMigrationGuard
|
||||
bind:this={ddlGuard}
|
||||
workspace={$workspaceStore}
|
||||
datatable={datatableForMigrations}
|
||||
/>
|
||||
{/if}
|
||||
{#if !editor}
|
||||
<div class="inset-0 absolute overflow-clip">
|
||||
<FakeMonacoPlaceHolder {code} lineNumbersWidth={51} />
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
import { useWorkspaceDrafts } from '$lib/workspaceDrafts.svelte'
|
||||
import { devLabelWord } from '$lib/utils/devWorkspaceLabel'
|
||||
|
||||
let loading = $state(false)
|
||||
let comparison: WorkspaceComparison | undefined = $state(undefined)
|
||||
@@ -170,7 +171,9 @@
|
||||
<GitFork class="w-4 h-4 text-accent" />
|
||||
<div class="text-sm">
|
||||
<span class="font-medium text-blue-900 dark:text-blue-100">
|
||||
{isDevWorkspace ? 'Dev workspace of' : 'Fork of'}
|
||||
{isDevWorkspace
|
||||
? `${devLabelWord(currentWorkspaceData?.dev_workspace_label)} workspace of`
|
||||
: 'Fork of'}
|
||||
<b>{parentWorkspaceData?.name}</b> ({parentWorkspaceId})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
isRuleActive
|
||||
} from '$lib/workspaceProtectionRules.svelte'
|
||||
import { findCanonicalDevWorkspace } from '$lib/utils/workspaceHierarchy'
|
||||
import { devLabelNoun } from '$lib/utils/devWorkspaceLabel'
|
||||
import { canCreateFork } from '$lib/utils/editInFork'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
import { Alert, Button } from './common'
|
||||
@@ -40,12 +41,14 @@
|
||||
<div class="my-2">
|
||||
<Alert
|
||||
type="info"
|
||||
title={canonicalDev ? 'Edits happen in the dev workspace' : 'Workspace protection active'}
|
||||
title={canonicalDev
|
||||
? `Edits happen in the ${devLabelNoun(canonicalDev.dev_workspace_label)}`
|
||||
: 'Workspace protection active'}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if canonicalDev}
|
||||
<p>
|
||||
Edits to this workspace are made in its dev workspace
|
||||
Edits to this workspace are made in its {devLabelNoun(canonicalDev.dev_workspace_label)}
|
||||
<b>{canonicalDev.name}</b> ({canonicalDev.id}) and promoted here.
|
||||
</p>
|
||||
<div>
|
||||
@@ -58,7 +61,7 @@
|
||||
if (canonicalDev) switchWorkspace(canonicalDev.id)
|
||||
}}
|
||||
>
|
||||
Go to dev workspace
|
||||
Go to {devLabelNoun(canonicalDev.dev_workspace_label)}
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -793,7 +793,17 @@
|
||||
args = nargs
|
||||
}
|
||||
|
||||
export async function runTest(opts?: { cascade?: boolean }) {
|
||||
export async function runTest(opts?: { cascade?: boolean; skipDdlGuard?: boolean }) {
|
||||
// Intercept DDL statements (offer to turn them into data table migrations)
|
||||
// on every run path, not just the editor's Cmd+Enter. `skipDdlGuard` is set
|
||||
// by the Cmd+Enter action, which already guarded before calling us.
|
||||
if (!opts?.skipDdlGuard) {
|
||||
if ((await editor?.guardDdlBeforeRun()) === false) return
|
||||
// The guard may have rewritten the code (migrated statements stripped);
|
||||
// `editorCode` is kept in sync by the editor binding, so mirror the
|
||||
// on:change handler and pull it into `code` before we run.
|
||||
if (activeModuleTab === null) code = editorCode
|
||||
}
|
||||
// When the caller forces a cascade choice (e.g. the canvas runnable
|
||||
// menu's "Run + trigger N downstream"), also flip the persistent
|
||||
// `cascadeDownstream` state so the split button's label/icon reflect
|
||||
@@ -2677,7 +2687,8 @@
|
||||
} else {
|
||||
await inferModuleSchema()
|
||||
}
|
||||
runTest()
|
||||
// The Editor already ran the DDL guard before invoking this action.
|
||||
runTest({ skipDdlGuard: true })
|
||||
}}
|
||||
formatAction={async () => {
|
||||
if (activeModuleTab === null) {
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
loadAsync = false,
|
||||
key,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
minHeight = 1000,
|
||||
renderLineHighlight = 'none',
|
||||
suggestion
|
||||
@@ -123,6 +124,9 @@
|
||||
initialCursorPos?: IPosition
|
||||
key?: string
|
||||
disabled?: boolean
|
||||
/** Read-only Monaco mode: not editable, but still scrollable/selectable
|
||||
* (unlike `disabled`, which makes the editor non-interactive). */
|
||||
readOnly?: boolean
|
||||
minHeight?: number
|
||||
renderLineHighlight?: 'all' | 'line' | 'gutter' | 'none'
|
||||
suggestion?: string
|
||||
@@ -239,6 +243,9 @@
|
||||
lineNumbers: $relativeLineNumbers ? 'relative' : 'on'
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
editor?.updateOptions({ readOnly })
|
||||
})
|
||||
|
||||
function onVimDisable() {
|
||||
vimDisposable?.dispose()
|
||||
@@ -342,6 +349,7 @@
|
||||
),
|
||||
model,
|
||||
...(yPadding !== undefined ? { padding: { bottom: yPadding, top: yPadding } } : {}),
|
||||
readOnly,
|
||||
renderLineHighlight,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 2,
|
||||
|
||||
@@ -1,49 +1,3 @@
|
||||
<script module lang="ts">
|
||||
// code may be composed of many sql statements separated by ';'
|
||||
// this function splits them while taking into account that ';' may not
|
||||
// be the end of a statement (string or escaped)
|
||||
function splitSqlStatements(code: string) {
|
||||
const statements: string[] = []
|
||||
let currentStatement = ''
|
||||
let inSingleQuote = false
|
||||
let inDoubleQuote = false
|
||||
let inBacktick = false
|
||||
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
const char = code[i]
|
||||
const prevChar = i > 0 ? code[i - 1] : null
|
||||
|
||||
if (char === "'" && !inDoubleQuote && !inBacktick && prevChar !== '\\') {
|
||||
inSingleQuote = !inSingleQuote
|
||||
} else if (char === '"' && !inSingleQuote && !inBacktick && prevChar !== '\\') {
|
||||
inDoubleQuote = !inDoubleQuote
|
||||
} else if (char === '`' && !inSingleQuote && !inDoubleQuote && prevChar !== '\\') {
|
||||
inBacktick = !inBacktick
|
||||
}
|
||||
|
||||
if (char === ';' && !inSingleQuote && !inDoubleQuote && !inBacktick) {
|
||||
statements.push(currentStatement.trim())
|
||||
currentStatement = ''
|
||||
} else {
|
||||
currentStatement += char
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStatement.trim()) {
|
||||
statements.push(currentStatement.trim())
|
||||
}
|
||||
|
||||
return statements
|
||||
}
|
||||
|
||||
function pruneComments(code: string) {
|
||||
return code
|
||||
.replace(/--.*?(\r?\n|$)/g, '')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
@@ -57,15 +11,29 @@
|
||||
import { getDatabaseArg, getDbType } from './dbOps'
|
||||
import type { DbInput } from './dbTypes'
|
||||
import { wrapDucklakeQuery } from './ducklake'
|
||||
import { splitSqlStatements, pruneComments } from './sqlDdl'
|
||||
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
|
||||
|
||||
type Props = {
|
||||
input: DbInput
|
||||
onData: (data: Record<string, any>[]) => void
|
||||
placeholderTableName?: string
|
||||
/** Called after a migration is run via the DDL guard, so the schema view
|
||||
* can be refreshed to reflect the applied change. */
|
||||
onSchemaChange?: () => void
|
||||
}
|
||||
let { input, onData, placeholderTableName }: Props = $props()
|
||||
let { input, onData, placeholderTableName, onSchemaChange }: Props = $props()
|
||||
let dbType = $derived(getDbType(input))
|
||||
|
||||
// A datatable REPL targets `datatable://<name>`; surface DDL statements as
|
||||
// migration prompts instead of running them ad-hoc.
|
||||
let datatableName = $derived(
|
||||
input.type === 'database' && input.resourcePath.startsWith('datatable://')
|
||||
? input.resourcePath.slice('datatable://'.length).split('/')[0]
|
||||
: undefined
|
||||
)
|
||||
let ddlGuard = $state<DdlMigrationGuard | undefined>(undefined)
|
||||
|
||||
const DEFAULT_SQL = 'SELECT * FROM _'
|
||||
let code = $state(DEFAULT_SQL)
|
||||
$effect(() => {
|
||||
@@ -81,6 +49,18 @@
|
||||
async function run({ doPostgresRowToJsonFix }: { doPostgresRowToJsonFix?: boolean } = {}) {
|
||||
if (isRunning || !$workspaceStore) return
|
||||
const READ_OPS = ['SELECT', 'WITH', 'SHOW', 'EXPLAIN', 'DESCRIBE']
|
||||
|
||||
// On a datatable, intercept DDL statements and offer to make them
|
||||
// migrations; the user may strip some, leaving the rest to run.
|
||||
if (datatableName && ddlGuard && !doPostgresRowToJsonFix) {
|
||||
const res = await ddlGuard.guard(code)
|
||||
// A migration that was created and run changes the schema; refresh it.
|
||||
if (res.ranMigration) onSchemaChange?.()
|
||||
if (!res.proceed) return
|
||||
if (res.code !== code) code = res.code
|
||||
if (pruneComments(code).trim() === '') return
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
try {
|
||||
const statements = splitSqlStatements(pruneComments(code))
|
||||
@@ -155,7 +135,10 @@
|
||||
isRunning = false
|
||||
return await run({ doPostgresRowToJsonFix: true })
|
||||
}
|
||||
sendUserToast('Error running query: ' + (e.message ?? e.error.message), true)
|
||||
sendUserToast(
|
||||
'Error running query: ' + (e?.body ?? e?.message ?? e?.error?.message ?? e),
|
||||
true
|
||||
)
|
||||
} finally {
|
||||
isRunning = false
|
||||
}
|
||||
@@ -197,3 +180,7 @@
|
||||
/>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
{#if datatableName && $workspaceStore}
|
||||
<DdlMigrationGuard bind:this={ddlGuard} workspace={$workspaceStore} datatable={datatableName} />
|
||||
{/if}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user