diff --git a/.githooks/pre-push b/.githooks/pre-push index 9fa373a0..c86dc2c6 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -24,12 +24,18 @@ GIT_HOOK_RUN_LINT="${GIT_HOOK_RUN_LINT:-1}" GIT_HOOK_LINT_CMD="${GIT_HOOK_LINT_CMD:-rdt lint}" GIT_HOOK_LINT_MAX_ERR_LINES="${GIT_HOOK_LINT_MAX_ERR_LINES:-50}" +GIT_HOOK_RUN_KNIP="${GIT_HOOK_RUN_KNIP:-1}" +GIT_HOOK_KNIP_CMD="${GIT_HOOK_KNIP_CMD:-rdt knip}" +GIT_HOOK_KNIP_MAX_ERR_LINES="${GIT_HOOK_KNIP_MAX_ERR_LINES:-50}" + GIT_HOOK_RUN_TS_BINDINGS="${GIT_HOOK_RUN_TS_BINDINGS:-1}" GIT_HOOK_TS_BINDINGS_CMD="${GIT_HOOK_TS_BINDINGS_CMD:-rdt generate-ts-check}" GIT_HOOK_TS_BINDINGS_MAX_ERR_LINES="${GIT_HOOK_TS_BINDINGS_MAX_ERR_LINES:-50}" GIT_HOOK_RUN_COMMIT_LINT="${GIT_HOOK_RUN_COMMIT_LINT:-1}" +GIT_HOOK_RUN_BRANCH_NAME="${GIT_HOOK_RUN_BRANCH_NAME:-1}" + log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*" } @@ -130,8 +136,8 @@ normalize_bool_10() { load_env_overrides_all() { local root; root="$(repo_root)" local files=(".env.docker" ".env.local" ".env.docker.local") - local allowed=" GIT_HOOK_REMOTE GIT_HOOK_LOCK_FILE GIT_HOOK_RUN_FMT GIT_HOOK_FMT_CMD GIT_HOOK_FMT_MAX_ERR_LINES GIT_HOOK_RUN_CLIPPY GIT_HOOK_CLIPPY_CMD GIT_HOOK_CLIPPY_MAX_ERR_LINES GIT_HOOK_RUN_TEST GIT_HOOK_TEST_CMD GIT_HOOK_TEST_MAX_ERR_LINES GIT_HOOK_RUN_TEST_FE GIT_HOOK_TEST_FE_CMD GIT_HOOK_TEST_FE_MAX_ERR_LINES GIT_HOOK_RUN_LINT GIT_HOOK_LINT_CMD GIT_HOOK_LINT_MAX_ERR_LINES GIT_HOOK_RUN_TS_BINDINGS GIT_HOOK_TS_BINDINGS_CMD GIT_HOOK_TS_BINDINGS_MAX_ERR_LINES GIT_HOOK_RUN_COMMIT_LINT GIT_HOOK_SKIP " - local booleans=" GIT_HOOK_RUN_FMT GIT_HOOK_RUN_CLIPPY GIT_HOOK_RUN_TEST GIT_HOOK_RUN_TEST_FE GIT_HOOK_RUN_LINT GIT_HOOK_RUN_TS_BINDINGS GIT_HOOK_RUN_COMMIT_LINT GIT_HOOK_SKIP " + local allowed=" GIT_HOOK_REMOTE GIT_HOOK_LOCK_FILE GIT_HOOK_RUN_FMT GIT_HOOK_FMT_CMD GIT_HOOK_FMT_MAX_ERR_LINES GIT_HOOK_RUN_CLIPPY GIT_HOOK_CLIPPY_CMD GIT_HOOK_CLIPPY_MAX_ERR_LINES GIT_HOOK_RUN_TEST GIT_HOOK_TEST_CMD GIT_HOOK_TEST_MAX_ERR_LINES GIT_HOOK_RUN_TEST_FE GIT_HOOK_TEST_FE_CMD GIT_HOOK_TEST_FE_MAX_ERR_LINES GIT_HOOK_RUN_LINT GIT_HOOK_LINT_CMD GIT_HOOK_LINT_MAX_ERR_LINES GIT_HOOK_RUN_KNIP GIT_HOOK_KNIP_CMD GIT_HOOK_KNIP_MAX_ERR_LINES GIT_HOOK_RUN_TS_BINDINGS GIT_HOOK_TS_BINDINGS_CMD GIT_HOOK_TS_BINDINGS_MAX_ERR_LINES GIT_HOOK_RUN_COMMIT_LINT GIT_HOOK_RUN_BRANCH_NAME GIT_HOOK_SKIP " + local booleans=" GIT_HOOK_RUN_FMT GIT_HOOK_RUN_CLIPPY GIT_HOOK_RUN_TEST GIT_HOOK_RUN_TEST_FE GIT_HOOK_RUN_LINT GIT_HOOK_RUN_KNIP GIT_HOOK_RUN_TS_BINDINGS GIT_HOOK_RUN_COMMIT_LINT GIT_HOOK_RUN_BRANCH_NAME GIT_HOOK_SKIP " for f in "${files[@]}"; do local path="${root}/${f}" @@ -208,6 +214,35 @@ ensure_clean_working_tree() { fi } +validate_branch_name() { + local branch="$1" + + if [[ "${GIT_HOOK_RUN_BRANCH_NAME}" != "1" ]]; then + return 0 + fi + + log "Validating branch name" + + local regex='^(main|master|develop|(release-please|dependabot|renovate)(--|/).+|(feat|fix|hotfix|chore|docs|refactor|perf|test|build|ci|style|revert|release)/[a-z0-9][a-z0-9._-]*)$' + + if [[ ! "$branch" =~ $regex ]]; then + log "Branch '${branch}' does not follow the naming convention." + log "" + log "Expected: /" + log "Types: feat, fix, hotfix, chore, docs, refactor, perf, test, build, ci, style, revert, release" + log "Description: lowercase alphanumeric + . _ - (must start alphanumeric; no uppercase)" + log "Examples:" + log " feat/user-authentication" + log " fix/token-expiration" + log " hotfix/prod-crash-2026-04" + log "" + log "Exempt branches: main, master, develop, release-please--*, dependabot/*, renovate/*" + fail "Branch name validation failed" + fi + + log "Branch name '${branch}' is valid" +} + validate_conventional_commits() { local base_branch="$1" local current_branch="$2" @@ -390,6 +425,30 @@ run_lint() { log "ESLint (admin) passed" } +run_knip() { + local tmp_file + tmp_file="$(mktemp)" + + log "Running knip (unused-export / dead-code gate)" + + set +e + ${GIT_HOOK_KNIP_CMD} 2>&1 | tee "${tmp_file}" + local knip_status=$? + set -e + + if (( knip_status != 0 )); then + local total_lines + total_lines="$(wc -l < "${tmp_file}" | tr -d ' ')" + log "Knip errors (showing first ${GIT_HOOK_KNIP_MAX_ERR_LINES} of ${total_lines}):" + head -n "${GIT_HOOK_KNIP_MAX_ERR_LINES}" "${tmp_file}" + rm -f "${tmp_file}" + fail "Knip failed. Remove the unused exports/files it reported, or add a justified entry to fe/knip.json." + fi + + rm -f "${tmp_file}" + log "Knip passed" +} + run_ts_bindings() { local tmp_file tmp_file="$(mktemp)" @@ -435,6 +494,10 @@ run_quality_checks() { run_lint fi + if [[ "${GIT_HOOK_RUN_KNIP}" == "1" ]]; then + run_knip + fi + if [[ "${GIT_HOOK_RUN_TS_BINDINGS}" == "1" ]]; then run_ts_bindings fi @@ -452,6 +515,8 @@ main() { start_branch="$(get_current_branch_name)" log "Current branch: ${start_branch}" + validate_branch_name "${start_branch}" + ensure_clean_working_tree fetch_remote_all diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11eed9db..70778b5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,83 @@ jobs: echo "backend=true" >> $GITHUB_OUTPUT echo "frontend=true" >> $GITHUB_OUTPUT + # ============================================================================ + # Commit Lint - Validates Conventional Commits on every PR + # ============================================================================ + commit-lint: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate Conventional Commits + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + conventional_pattern='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+' + merge_pattern='^Merge ' + invalid_commits=() + while IFS= read -r commit_sha; do + [[ -z "$commit_sha" ]] && continue + subject="$(git log -1 --format='%s' "$commit_sha")" + if [[ "$subject" =~ $merge_pattern ]]; then + continue + fi + if ! [[ "$subject" =~ $conventional_pattern ]]; then + invalid_commits+=("$commit_sha: $subject") + fi + done < <(git rev-list "$BASE_SHA..$HEAD_SHA") + + if [[ ${#invalid_commits[@]} -gt 0 ]]; then + echo "::error::The following commits do not follow Conventional Commits format:" + for msg in "${invalid_commits[@]}"; do + echo " - $msg" + done + echo "" + echo "Expected format: [optional scope][!]: " + echo "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert" + echo "Examples:" + echo " feat: add user authentication" + echo " fix(auth): resolve token expiration issue" + echo " feat!: breaking change to API" + exit 1 + fi + + echo "All commits follow Conventional Commits format" + + # ============================================================================ + # Branch Name Validation - Runs on every PR + # ============================================================================ + branch-name: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Validate branch name + env: + BRANCH_NAME: ${{ github.head_ref }} + run: | + regex='^(main|master|develop|(release-please|dependabot|renovate)(--|/).+|(feat|fix|hotfix|chore|docs|refactor|perf|test|build|ci|style|revert|release)/[a-z0-9][a-z0-9._-]*)$' + if [[ ! "$BRANCH_NAME" =~ $regex ]]; then + echo "::error::Branch name '$BRANCH_NAME' does not follow the naming convention." + echo "" + echo "Expected: /" + echo "Types: feat, fix, hotfix, chore, docs, refactor, perf, test, build, ci, style, revert, release" + echo "Description: lowercase alphanumeric + . _ - (must start alphanumeric; no uppercase)" + echo "" + echo "Examples:" + echo " feat/user-authentication" + echo " fix/token-expiration" + echo " hotfix/prod-crash-2026-04" + echo "" + echo "Exempt branches: main, master, develop, release-please--*, dependabot/*, renovate/*" + exit 1 + fi + echo "Branch name '$BRANCH_NAME' is valid" + # ============================================================================ # Backend Jobs - Only run when backend files change # ============================================================================ @@ -304,6 +381,27 @@ jobs: with: workspaces: . + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: fe/package-lock.json + + - name: Cache fe/node_modules + id: cache-node-modules + uses: actions/cache@v4 + with: + path: fe/node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('fe/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-modules- + + - name: Install fe dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + working-directory: fe + run: npm ci + - name: Generate TS bindings env: SQLX_OFFLINE: true @@ -311,6 +409,10 @@ jobs: TS_RS_EXPORT_DIR=${{ github.workspace }}/fe/src/types/generated cargo test --workspace export_bindings 2>&1 cargo run -p r_data_core_core --bin export_validation_constants > ./fe/src/types/generated/validation.ts + - name: Format generated bindings (prettier) + working-directory: fe + run: npx prettier --write --log-level warn 'src/types/generated/**/*.ts' + - name: Check for stale bindings run: | if ! git diff --exit-code fe/src/types/generated/; then @@ -381,6 +483,10 @@ jobs: working-directory: fe run: npm run lint + - name: Knip + working-directory: fe + run: npm run knip + frontend-tests: needs: [changes, frontend-prepare] if: ${{ needs.changes.outputs.frontend == 'true' }} @@ -647,6 +753,8 @@ jobs: ci-gate: needs: - changes + - branch-name + - commit-lint - backend-clippy - backend-format - backend-audit @@ -661,6 +769,24 @@ jobs: if: ${{ always() }} runs-on: ubuntu-latest steps: + - name: Check branch name result + if: ${{ github.event_name == 'pull_request' }} + run: | + if [[ "${{ needs.branch-name.result }}" != "success" ]]; then + echo "Branch name check failed" + exit 1 + fi + echo "Branch name check passed" + + - name: Check commit lint result + if: ${{ github.event_name == 'pull_request' }} + run: | + if [[ "${{ needs.commit-lint.result }}" != "success" ]]; then + echo "Commit lint check failed" + exit 1 + fi + echo "Commit lint check passed" + - name: Check backend results if: ${{ needs.changes.outputs.backend == 'true' }} run: | diff --git a/.rusty_dev_tool/config.toml b/.rusty_dev_tool/config.toml index dd444355..cfc0b695 100644 --- a/.rusty_dev_tool/config.toml +++ b/.rusty_dev_tool/config.toml @@ -37,11 +37,15 @@ no_docker_compose = true "description" = "Removes all E2E test data (e2e_* users, keys, roles, workflows, entity definitions, and dynamic tables) from the local database" [commands.generate-ts] "alias" = "generate-ts" - "execution" = "TS_RS_EXPORT_DIR=$(pwd)/fe/src/types/generated cargo test --workspace export_bindings 2>&1 && cargo run -p r_data_core_core --bin export_validation_constants > ./fe/src/types/generated/validation.ts" + "execution" = "TS_RS_EXPORT_DIR=$(pwd)/fe/src/types/generated cargo test --workspace export_bindings 2>&1 && cargo run -p r_data_core_core --bin export_validation_constants > ./fe/src/types/generated/validation.ts && docker compose exec -T node npx prettier --write --log-level warn 'src/types/generated/**/*.ts'" "description" = "Generates TypeScript type bindings and validation constants from Rust structs" [commands.generate-ts-check] "alias" = "generate-ts-check" - "execution" = "TS_RS_EXPORT_DIR=$(pwd)/fe/src/types/generated cargo test --workspace export_bindings 2>&1 && cargo run -p r_data_core_core --bin export_validation_constants > ./fe/src/types/generated/validation.ts && git diff --exit-code fe/src/types/generated/" + "execution" = "TS_RS_EXPORT_DIR=$(pwd)/fe/src/types/generated cargo test --workspace export_bindings 2>&1 && cargo run -p r_data_core_core --bin export_validation_constants > ./fe/src/types/generated/validation.ts && docker compose exec -T node npx prettier --write --log-level warn 'src/types/generated/**/*.ts' && git diff --exit-code fe/src/types/generated/" "description" = "Generates TS bindings and fails if any generated files differ from committed versions" + [commands.knip] + "alias" = "knip" + "execution" = "docker compose exec -T node npx knip" + "description" = "Runs knip to enforce that every generated/exported symbol is consumed (fails on unused files, exports, or deps)" # Mandatory node [environments] diff --git a/README.md b/README.md index 8def674c..aa07ea68 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,15 @@ docker pull ghcr.io/bentbr/r-data-core-worker:latest docker pull ghcr.io/bentbr/r-data-core-maintenance:latest ``` +### Example deployments + +Runnable example setups for deploying RDataCore to common targets live in [`docs/examples/`](./docs/examples/): + +- **[Docker Compose](./docs/examples/docker-compose/)** — single-host deployment using pre-built images from GHCR. +- **[Kubernetes + Longhorn](./docs/examples/kubernetes/)** — namespace-scoped manifests with Longhorn-backed persistent storage, Ingress, HPA, and a migrations Job. + +> These are **examples only, with no warranty**. Review before adapting to your environment. + ## Configuration ### Required Environment Variables diff --git a/crates/api/src/admin/api_keys/models.rs b/crates/api/src/admin/api_keys/models.rs index f6491b1a..338f3db2 100644 --- a/crates/api/src/admin/api_keys/models.rs +++ b/crates/api/src/admin/api_keys/models.rs @@ -13,11 +13,11 @@ pub struct CreateApiKeyRequest { /// Name of the API key pub name: String, /// Optional description for the API key + #[serde(default)] pub description: Option, /// Number of days until expiration (default: 365) #[serde(default)] - #[ts(type = "number | null")] - pub expires_in_days: Option, + pub expires_in_days: Option, } /// Response containing API key information diff --git a/crates/api/src/admin/api_keys/routes.rs b/crates/api/src/admin/api_keys/routes.rs index 04f27243..5d11cba4 100644 --- a/crates/api/src/admin/api_keys/routes.rs +++ b/crates/api/src/admin/api_keys/routes.rs @@ -184,7 +184,7 @@ pub async fn create_api_key( let description = req.description.clone().unwrap_or_default(); let expires_in_days = req .expires_in_days - .map_or(365, |v| i32::try_from(v).unwrap_or(365)); + .map_or(365, |v| i32::try_from(v).unwrap_or(i32::MAX)); match service .create_api_key(&req.name, &description, creator_uuid, expires_in_days) diff --git a/crates/api/src/admin/auth/models.rs b/crates/api/src/admin/auth/models.rs index 0f55286d..e4edf137 100644 --- a/crates/api/src/admin/auth/models.rs +++ b/crates/api/src/admin/auth/models.rs @@ -8,10 +8,21 @@ use utoipa::ToSchema; use validator::Validate; /// Empty request body for endpoints that don't require any input -#[derive(Debug, Deserialize, ToSchema, TS)] -#[ts(export)] +#[derive(Debug, Deserialize, ToSchema)] pub struct EmptyRequest {} +/// Response body for `GET /admin/api/v1/auth/permissions` +#[derive(Debug, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] +pub struct UserPermissionsResponse { + /// Whether the caller is a super admin (all permissions granted) + pub is_super_admin: bool, + /// Flat list of `namespace:permission_type` strings the caller holds + pub permissions: Vec, + /// Router paths the caller is allowed to navigate to + pub allowed_routes: Vec, +} + /// Refresh token request body #[derive(Debug, Deserialize, Serialize, ToSchema, TS)] #[ts(export)] @@ -93,8 +104,7 @@ pub struct AdminLoginResponse { } /// Admin registration request body -#[derive(Debug, Deserialize, ToSchema, Validate, TS)] -#[ts(export)] +#[derive(Debug, Deserialize, ToSchema, Validate)] pub struct AdminRegisterRequest { /// Username #[validate(length(min = 3, message = "Username must be at least 3 characters"))] @@ -121,8 +131,7 @@ pub struct AdminRegisterRequest { } /// Admin registration response body -#[derive(Debug, Serialize, ToSchema, TS)] -#[ts(export)] +#[derive(Debug, Serialize, ToSchema)] pub struct AdminRegisterResponse { /// User UUID pub uuid: String, diff --git a/crates/api/src/admin/auth/routes.rs b/crates/api/src/admin/auth/routes.rs index dc5ded22..95bd5a7c 100644 --- a/crates/api/src/admin/auth/routes.rs +++ b/crates/api/src/admin/auth/routes.rs @@ -18,6 +18,7 @@ use r_data_core_persistence::{RefreshTokenRepository, RefreshTokenRepositoryTrai use crate::admin::auth::models::{ AdminLoginRequest, AdminLoginResponse, AdminRegisterRequest, ForgotPasswordRequest, LogoutRequest, RefreshTokenRequest, RefreshTokenResponse, ResetPasswordRequest, + UserPermissionsResponse, }; use validator::Validate; @@ -631,7 +632,7 @@ pub async fn admin_revoke_all_tokens( path = "/admin/api/v1/auth/permissions", tag = "admin-auth", responses( - (status = 200, description = "User permissions and allowed routes", body = serde_json::Value), + (status = 200, description = "User permissions and allowed routes", body = UserPermissionsResponse), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ), @@ -645,9 +646,8 @@ pub async fn get_user_permissions(auth: RequiredAuth) -> impl Responder { let claims = &auth.0; - // Use auth service to get user permissions let auth_service = AuthService::new(); - let response = auth_service.get_user_permissions( + let (is_super_admin, permissions, allowed_routes) = auth_service.get_user_permissions( claims.is_super_admin, &claims.permissions, |namespace, perm_type| { @@ -655,7 +655,11 @@ pub async fn get_user_permissions(auth: RequiredAuth) -> impl Responder { }, ); - ApiResponse::ok(response) + ApiResponse::ok(UserPermissionsResponse { + is_super_admin, + permissions, + allowed_routes, + }) } /// Forgot password endpoint — initiates the password reset flow diff --git a/crates/api/src/admin/email_templates/models.rs b/crates/api/src/admin/email_templates/models.rs index 72515861..8e207940 100644 --- a/crates/api/src/admin/email_templates/models.rs +++ b/crates/api/src/admin/email_templates/models.rs @@ -11,8 +11,8 @@ use uuid::Uuid; #[ts(export)] pub struct EmailTemplateListQuery { /// Filter by template type: "system" or "workflow" - #[serde(rename = "type")] - #[ts(type = "string | null")] + #[serde(default, rename = "type")] + #[ts(rename = "type")] pub template_type: Option, } @@ -40,6 +40,7 @@ pub struct CreateEmailTemplateRequest { #[ts(export)] pub struct UpdateEmailTemplateRequest { /// New display name (only honoured for workflow templates) + #[serde(default)] pub name: Option, /// Updated subject line pub subject_template: String, diff --git a/crates/api/src/admin/entity_definitions/models.rs b/crates/api/src/admin/entity_definitions/models.rs index ca2cbe94..f3cbc2a2 100644 --- a/crates/api/src/admin/entity_definitions/models.rs +++ b/crates/api/src/admin/entity_definitions/models.rs @@ -128,7 +128,6 @@ pub enum FieldConstraints { /// Schema for options source in `OpenAPI` docs /// Defines how to populate options for `Select` and `MultiSelect` fields #[derive(Debug, Serialize, Deserialize, ToSchema, TS)] -#[ts(export)] #[serde(tag = "type")] pub enum OptionsSourceSchema { /// Fixed list of options defined statically @@ -155,7 +154,6 @@ pub enum OptionsSourceSchema { /// Schema for select options in `OpenAPI` docs /// Used for defining individual options in fixed option lists #[derive(Debug, Serialize, Deserialize, ToSchema, TS)] -#[ts(export)] pub struct SelectOptionSchema { /// Option value (stored in database) pub value: String, @@ -295,14 +293,12 @@ pub struct EntityDefinitionSchema { } /// Response for listing entity definitions -#[derive(Debug, Serialize, ToSchema, TS)] -#[ts(export)] +#[derive(Debug, Serialize, ToSchema)] #[schema(title = "EntityDefinitionListResponse")] pub struct EntityDefinitionListResponse { /// List of entity definitions pub items: Vec, /// Total number of items - #[ts(type = "number")] pub total: i64, } diff --git a/crates/api/src/admin/entity_definitions/routes.rs b/crates/api/src/admin/entity_definitions/routes.rs index 1470fb68..11b1420b 100644 --- a/crates/api/src/admin/entity_definitions/routes.rs +++ b/crates/api/src/admin/entity_definitions/routes.rs @@ -8,6 +8,7 @@ use r_data_core_core::permissions::role::{PermissionType, ResourceNamespace}; use serde::Serialize; use serde_json::json; use time::OffsetDateTime; +use ts_rs::TS; use uuid::Uuid; use crate::admin::entity_definitions::conversions::entity_definition_to_schema_model; @@ -516,12 +517,19 @@ pub fn register_routes(cfg: &mut web::ServiceConfig) { .service(get_entity_definition_version); } -#[derive(Debug, Serialize, ToSchema)] -struct EntityFieldInfo { - name: String, - r#type: String, - required: bool, - system: bool, +/// Per-field metadata returned by `GET /entity-definitions/{type}/fields` +#[derive(Debug, Serialize, ToSchema, TS)] +#[ts(export)] +pub struct EntityFieldInfo { + /// Field column name + pub name: String, + /// Field type (as declared in the entity definition) + #[serde(rename = "type")] + pub r#type: String, + /// Whether the field is required + pub required: bool, + /// Whether the field is a BE-managed system field (not user-definable) + pub system: bool, } /// List all fields for an entity definition by `entity_type`, including system fields diff --git a/crates/api/src/admin/meta/models.rs b/crates/api/src/admin/meta/models.rs index dcf6f1ab..0ac6aad8 100644 --- a/crates/api/src/admin/meta/models.rs +++ b/crates/api/src/admin/meta/models.rs @@ -1,6 +1,7 @@ #![deny(clippy::all, clippy::pedantic, clippy::nursery, warnings)] use serde::{Deserialize, Serialize}; +use ts_rs::TS; use utoipa::ToSchema; use r_data_core_persistence::dashboard_stats_repository_trait::{ @@ -10,25 +11,30 @@ use r_data_core_persistence::dashboard_stats_repository_trait::{ }; /// Entity count for a specific type -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct EntityTypeCount { /// Entity type name pub entity_type: String, /// Count of entities of this type + #[ts(type = "number")] pub count: i64, } /// Entity statistics -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct EntityStats { /// Total count of all entities across all types + #[ts(type = "number")] pub total: i64, /// Breakdown by entity type pub by_type: Vec, } /// Workflow with its latest run status -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct WorkflowWithLatestStatus { /// Workflow UUID pub uuid: String, @@ -39,24 +45,29 @@ pub struct WorkflowWithLatestStatus { } /// Workflow statistics -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct WorkflowStats { /// Total count of workflows + #[ts(type = "number")] pub total: i64, /// List of workflows with their latest run status pub workflows: Vec, } /// Dashboard statistics response -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct DashboardStats { /// Total count of entity definitions + #[ts(type = "number")] pub entity_definitions_count: i64, /// Entity statistics pub entities: EntityStats, /// Workflow statistics pub workflows: WorkflowStats, /// Count of online users (users with active refresh tokens) + #[ts(type = "number")] pub online_users_count: i64, } diff --git a/crates/api/src/admin/permissions/models.rs b/crates/api/src/admin/permissions/models.rs index a15711da..65e79910 100644 --- a/crates/api/src/admin/permissions/models.rs +++ b/crates/api/src/admin/permissions/models.rs @@ -140,8 +140,10 @@ pub struct CreateRoleRequest { /// Name of the role pub name: String, /// Optional description + #[serde(default)] pub description: Option, /// Whether this role grants super admin privileges + #[serde(default)] pub super_admin: Option, /// Direct permissions for this role pub permissions: Vec, @@ -154,8 +156,10 @@ pub struct UpdateRoleRequest { /// Name of the role pub name: String, /// Optional description + #[serde(default)] pub description: Option, /// Whether this role grants super admin privileges + #[serde(default)] pub super_admin: Option, /// Direct permissions for this role pub permissions: Vec, diff --git a/crates/api/src/admin/system/models.rs b/crates/api/src/admin/system/models.rs index cad84fb0..812fbb5f 100644 --- a/crates/api/src/admin/system/models.rs +++ b/crates/api/src/admin/system/models.rs @@ -11,13 +11,16 @@ use r_data_core_core::settings::{EntityVersioningSettings, WorkflowRunLogSetting /// /// This is a thin wrapper around the core `EntityVersioningSettings` type /// to add `OpenAPI` schema generation support. -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct EntityVersioningSettingsDto { /// Whether entity versioning is enabled pub enabled: bool, /// Maximum number of versions to keep per entity + #[ts(type = "number | null")] pub max_versions: Option, /// Maximum age in days for versions + #[ts(type = "number | null")] pub max_age_days: Option, } @@ -42,13 +45,16 @@ impl From for EntityVersioningSettings { } /// DTO for workflow run log settings (API layer wrapper) -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct WorkflowRunLogSettingsDto { /// Whether workflow run logs pruning is enabled pub enabled: bool, /// Maximum number of runs to keep per workflow + #[ts(type = "number | null")] pub max_runs: Option, /// Maximum age in days for workflow runs + #[ts(type = "number | null")] pub max_age_days: Option, } @@ -73,30 +79,43 @@ impl From for WorkflowRunLogSettings { } /// Request body for updating workflow run log settings -#[derive(Deserialize, Serialize, ToSchema)] +#[derive(Deserialize, Serialize, ToSchema, TS)] +#[ts(export)] pub struct UpdateWorkflowRunLogSettingsBody { /// Whether pruning is enabled + #[serde(default)] pub enabled: Option, /// Maximum number of runs to keep per workflow + #[serde(default)] + #[ts(type = "number | null")] pub max_runs: Option, /// Maximum age in days + #[serde(default)] + #[ts(type = "number | null")] pub max_age_days: Option, } /// Request body for updating settings -#[derive(Deserialize, Serialize, ToSchema)] +#[derive(Deserialize, Serialize, ToSchema, TS)] +#[ts(export)] pub struct UpdateSettingsBody { /// Whether versioning is enabled + #[serde(default)] pub enabled: Option, /// Maximum number of versions to keep + #[serde(default)] + #[ts(type = "number | null")] pub max_versions: Option, /// Maximum age in days + #[serde(default)] + #[ts(type = "number | null")] pub max_age_days: Option, } /// License state enumeration -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, TS)] #[serde(rename_all = "lowercase")] +#[ts(export, rename_all = "lowercase")] pub enum LicenseStateDto { /// No license key provided None, @@ -109,7 +128,8 @@ pub enum LicenseStateDto { } /// DTO for license status -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct LicenseStatusDto { /// License state pub state: LicenseStateDto, @@ -121,14 +141,17 @@ pub struct LicenseStatusDto { pub license_id: Option, /// Issue date (if license is present) #[serde(with = "time::serde::rfc3339::option")] + #[ts(type = "string | null")] pub issued_at: Option, /// Expiration date (if license is present and has expiration) #[serde(with = "time::serde::rfc3339::option")] + #[ts(type = "string | null")] pub expires_at: Option, /// License version pub version: Option, /// Verification timestamp #[serde(with = "time::serde::rfc3339")] + #[ts(type = "string")] pub verified_at: time::OffsetDateTime, /// Error message (only present if state is "error" or "invalid") pub error_message: Option, @@ -159,14 +182,14 @@ impl From for } } -/// Request body for license verification (internal API) +/// Request body for license verification (internal API — not FE-facing) #[derive(Debug, Deserialize)] pub struct LicenseVerificationRequest { /// License key to verify pub license_key: String, } -/// Response for license verification (internal API) +/// Response for license verification (internal API — not FE-facing) #[derive(Debug, Serialize)] pub struct LicenseVerificationResponse { /// Whether the license is valid @@ -190,23 +213,30 @@ pub struct CapabilitiesResponse { #[ts(export)] pub struct SystemLogQuery { /// Page number (1-based, default: 1) + #[serde(default)] + #[ts(type = "number | null")] pub page: Option, /// Items per page (default: 20, max: 100) + #[serde(default)] + #[ts(type = "number | null")] pub page_size: Option, /// Filter by log type - #[ts(type = "string | null")] + #[serde(default)] pub log_type: Option, /// Filter by resource type - #[ts(type = "string | null")] + #[serde(default)] pub resource_type: Option, /// Filter by status - #[ts(type = "string | null")] + #[serde(default)] pub status: Option, /// Filter by resource UUID + #[serde(default)] pub resource_uuid: Option, /// Filter logs created after this timestamp (ISO 8601) + #[serde(default)] pub date_from: Option, /// Filter logs created before this timestamp (ISO 8601) + #[serde(default)] pub date_to: Option, } @@ -270,7 +300,8 @@ impl From for SystemLogDto { } /// Component version information -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct ComponentVersionDto { /// Name of the component pub name: String, @@ -278,11 +309,13 @@ pub struct ComponentVersionDto { pub version: String, /// Last time this component was seen (ISO 8601) #[serde(with = "time::serde::rfc3339")] + #[ts(type = "string")] pub last_seen_at: time::OffsetDateTime, } /// System versions response -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct SystemVersionsDto { /// Core/API server version pub core: String, diff --git a/crates/api/src/admin/users/models.rs b/crates/api/src/admin/users/models.rs index 8ee8fd3c..e29cfc65 100644 --- a/crates/api/src/admin/users/models.rs +++ b/crates/api/src/admin/users/models.rs @@ -100,11 +100,14 @@ pub struct CreateUserRequest { /// Last name pub last_name: String, /// Role UUIDs to assign to this user (optional) + #[serde(default)] #[ts(type = "string[] | null")] pub role_uuids: Option>, /// Whether user is active + #[serde(default)] pub is_active: Option, /// Super admin flag + #[serde(default)] pub super_admin: Option, } @@ -114,20 +117,27 @@ pub struct CreateUserRequest { pub struct UpdateUserRequest { /// Email address (optional) #[validate(regex(path = *EMAIL_RE))] + #[serde(default)] pub email: Option, /// Password (optional, only set if provided) #[validate(length(min = 8))] + #[serde(default)] pub password: Option, /// First name (optional) + #[serde(default)] pub first_name: Option, /// Last name (optional) + #[serde(default)] pub last_name: Option, /// Role UUIDs to assign to this user (optional) + #[serde(default)] #[ts(type = "string[] | null")] pub role_uuids: Option>, /// Whether user is active (optional) + #[serde(default)] pub is_active: Option, /// Super admin flag (optional) + #[serde(default)] pub super_admin: Option, } diff --git a/crates/api/src/admin/workflows/models.rs b/crates/api/src/admin/workflows/models.rs index 3437d2a0..2abc27c5 100644 --- a/crates/api/src/admin/workflows/models.rs +++ b/crates/api/src/admin/workflows/models.rs @@ -89,6 +89,21 @@ pub struct WorkflowRunUpload { pub file: String, } +/// Response for `POST /workflows/{uuid}/run/upload` +#[derive(Debug, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] +pub struct WorkflowRunUploadResponse { + /// UUID of the newly-created workflow run + #[ts(type = "string")] + pub run_uuid: Uuid, + /// Number of items that were staged from the uploaded file + #[ts(type = "number")] + pub staged_items: i64, + /// Present only when the upload succeeded but the follow-up job enqueue failed + #[serde(default)] + pub warning: Option, +} + #[derive(Serialize, ToSchema, TS)] #[ts(export)] pub struct WorkflowVersionMeta { diff --git a/crates/api/src/admin/workflows/routes/runs.rs b/crates/api/src/admin/workflows/routes/runs.rs index 8007914a..c1eb06ef 100644 --- a/crates/api/src/admin/workflows/routes/runs.rs +++ b/crates/api/src/admin/workflows/routes/runs.rs @@ -8,7 +8,7 @@ use log::{error, info}; use serde_json::json; use uuid::Uuid; -use crate::admin::workflows::models::WorkflowRunLogDto; +use crate::admin::workflows::models::{WorkflowRunLogDto, WorkflowRunUploadResponse}; use crate::admin::workflows::routes::utils::handle_workflow_error; use crate::api_state::{ApiStateTrait, ApiStateWrapper}; use crate::auth::auth_enum::RequiredAuth; @@ -177,21 +177,24 @@ pub async fn run_workflow_now_upload( { Ok(()) => { info!("Successfully enqueued fetch job for uploaded workflow {workflow_uuid} (run: {run_uuid}, staged: {staged})"); - ApiResponse::::ok(serde_json::json!({ - "run_uuid": run_uuid, - "staged_items": staged - })) + ApiResponse::::ok(WorkflowRunUploadResponse { + run_uuid, + staged_items: staged, + warning: None, + }) } Err(e) => { error!( "Failed to enqueue fetch job for uploaded workflow {workflow_uuid} (run: {run_uuid}): {e}" ); - // Still return success for the upload, but log the enqueue failure - ApiResponse::::ok(serde_json::json!({ - "run_uuid": run_uuid, - "staged_items": staged, - "warning": "Upload succeeded but job enqueue failed - items may not be processed automatically" - })) + ApiResponse::::ok(WorkflowRunUploadResponse { + run_uuid, + staged_items: staged, + warning: Some( + "Upload succeeded but job enqueue failed - items may not be processed automatically" + .to_string(), + ), + }) } } } diff --git a/crates/api/src/public/entities/models.rs b/crates/api/src/public/entities/models.rs index 713207fa..63ff74ab 100644 --- a/crates/api/src/public/entities/models.rs +++ b/crates/api/src/public/entities/models.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use ts_rs::TS; use utoipa::ToSchema; use uuid::Uuid; @@ -28,16 +29,18 @@ pub struct EntityQuery { } /// Kind of browse node -#[derive(Debug, Serialize, Deserialize, ToSchema, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, ToSchema, TS, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] +#[ts(export, rename_all = "snake_case")] pub enum BrowseKind { Folder, File, } /// Node returned when browsing entities by virtual path -#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] +#[derive(Debug, Serialize, Deserialize, ToSchema, TS, Clone)] #[serde(rename_all = "snake_case")] +#[ts(export)] pub struct BrowseNode { /// "folder" or "file" pub kind: BrowseKind, @@ -46,6 +49,7 @@ pub struct BrowseNode { /// Full path for this item pub path: String, /// Present for files or folder-entities that exist as entities + #[ts(type = "string | null")] pub entity_uuid: Option, /// Type of the entity if present pub entity_type: Option, @@ -56,22 +60,29 @@ pub struct BrowseNode { } /// Version metadata for entity versions -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize, ToSchema, TS)] +#[ts(export)] pub struct VersionMeta { pub version_number: i32, #[serde(with = "time::serde::rfc3339")] + #[ts(type = "string")] pub created_at: time::OffsetDateTime, + #[ts(type = "string | null")] pub created_by: Option, pub created_by_name: Option, } /// Version payload containing the actual entity data -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize, ToSchema, TS)] +#[ts(export)] pub struct VersionPayload { pub version_number: i32, #[serde(with = "time::serde::rfc3339")] + #[ts(type = "string")] pub created_at: time::OffsetDateTime, + #[ts(type = "string | null")] pub created_by: Option, + #[ts(type = "unknown")] pub data: serde_json::Value, } diff --git a/crates/api/src/query/mod.rs b/crates/api/src/query/mod.rs index 70f74ebc..1d94b73f 100644 --- a/crates/api/src/query/mod.rs +++ b/crates/api/src/query/mod.rs @@ -1,24 +1,30 @@ -use serde::Deserialize; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::HashMap; +use ts_rs::TS; use utoipa::ToSchema; -/// Custom deserializer for converting string query parameters to i64 -fn deserialize_optional_i64<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - use serde::de::Error; +/// Helper that accepts either a plain integer (e.g. from JSON) or a string-encoded +/// integer (e.g. from a URL query string) and yields an `i64`. Used only by +/// `PaginationQuery`'s manual `Deserialize` impl — kept as a struct-level helper so no +/// field carries `#[serde(deserialize_with = ...)]` (which ts-rs cannot introspect and +/// emits a warning for). +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum IntOrStringInt { + Int(i64), + Str(String), +} - let value = Option::::deserialize(deserializer)?; - value.map_or_else( - || Ok(None), - |s| { - s.parse::().map(Some).map_err(|_| { - D::Error::custom(format!("invalid type: string \"{s}\", expected i64")) - }) - }, - ) +impl IntOrStringInt { + fn into_i64(self) -> Result { + match self { + Self::Int(n) => Ok(n), + Self::Str(s) => s + .parse::() + .map_err(|_| E::custom(format!("invalid type: string \"{s}\", expected i64"))), + } + } } /// Custom deserializer for converting string query parameters to bool @@ -55,29 +61,53 @@ where /// /// All parameters are optional and have sensible defaults. You can mix and match these parameters /// as needed for your use case. -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Serialize, ToSchema, TS)] +#[ts(export)] pub struct PaginationQuery { /// Page number (1-based) - defaults to 1 /// Use with `per_page` for page-based pagination - #[serde(deserialize_with = "deserialize_optional_i64", default)] + #[ts(type = "number | null")] pub page: Option, /// Items per page - defaults to 20, max 100 /// Use with `page` for page-based pagination - #[serde(deserialize_with = "deserialize_optional_i64", default)] + #[ts(type = "number | null")] pub per_page: Option, /// Limit (alternative to `per_page`) - defaults to 20, max 100 /// Use with `offset` for offset-based pagination - #[serde(deserialize_with = "deserialize_optional_i64", default)] + #[ts(type = "number | null")] pub limit: Option, /// Offset (alternative to page) - defaults to 0 /// Use with `limit` for offset-based pagination - #[serde(deserialize_with = "deserialize_optional_i64", default)] + #[ts(type = "number | null")] pub offset: Option, } +impl<'de> Deserialize<'de> for PaginationQuery { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Raw { + #[serde(default)] + page: Option, + #[serde(default)] + per_page: Option, + #[serde(default)] + limit: Option, + #[serde(default)] + offset: Option, + } + let raw = Raw::deserialize(deserializer)?; + Ok(Self { + page: raw.page.map(IntOrStringInt::into_i64).transpose()?, + per_page: raw.per_page.map(IntOrStringInt::into_i64).transpose()?, + limit: raw.limit.map(IntOrStringInt::into_i64).transpose()?, + offset: raw.offset.map(IntOrStringInt::into_i64).transpose()?, + }) + } +} + impl PaginationQuery { /// Validate pagination parameters /// @@ -236,11 +266,14 @@ impl PaginationQuery { } /// Standard sorting query parameters -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct SortingQuery { /// Field to sort by + #[serde(default)] pub sort_by: Option, /// Sort order (asc or desc) + #[serde(default)] pub sort_order: Option, } diff --git a/crates/api/src/response.rs b/crates/api/src/response.rs index 68938f66..fa4f3a5a 100644 --- a/crates/api/src/response.rs +++ b/crates/api/src/response.rs @@ -15,6 +15,7 @@ pub struct ValidationViolation { /// The error message for this field pub message: String, /// Optional error code (e.g., `"NOT_BLANK"`, `"NOT_NULL"`) + #[serde(default)] pub code: Option, } diff --git a/crates/core/src/permissions/role/mod.rs b/crates/core/src/permissions/role/mod.rs index e88cf3bd..785627fa 100644 --- a/crates/core/src/permissions/role/mod.rs +++ b/crates/core/src/permissions/role/mod.rs @@ -68,6 +68,7 @@ pub enum AccessLevel { /// /// Each namespace represents a different resource type that can have permissions. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)] +#[ts(export)] pub enum ResourceNamespace { /// Workflows namespace Workflows, diff --git a/crates/services/src/auth.rs b/crates/services/src/auth.rs index 45da2271..85e8287c 100644 --- a/crates/services/src/auth.rs +++ b/crates/services/src/auth.rs @@ -1,7 +1,6 @@ #![deny(clippy::all, clippy::pedantic, clippy::nursery, warnings)] use r_data_core_core::permissions::role::{PermissionType, ResourceNamespace}; -use serde_json::Value; /// Service for authentication and authorization operations pub struct AuthService; @@ -13,22 +12,17 @@ impl AuthService { Self } - /// Get user's allowed routes and permissions + /// Get user's allowed routes and permissions. /// - /// # Arguments - /// * `is_super_admin` - Whether the user is a super admin - /// * `permissions` - User permissions from JWT - /// * `has_permission_fn` - Function to check if user has a specific permission - /// - /// # Returns - /// JSON value containing user permissions and allowed routes + /// Returns `(is_super_admin, permissions, allowed_routes)`. The API layer wraps this + /// in the `UserPermissionsResponse` DTO (which is TS-exported to the FE). #[must_use] pub fn get_user_permissions( &self, is_super_admin: bool, permissions: &[String], has_permission_fn: F, - ) -> Value + ) -> (bool, Vec, Vec) where F: Fn(&ResourceNamespace, &PermissionType) -> bool, { @@ -79,12 +73,7 @@ impl AuthService { }) .collect(); - // Build response - serde_json::json!({ - "is_super_admin": is_super_admin, - "permissions": permissions, - "allowed_routes": allowed_routes, - }) + (is_super_admin, permissions.to_vec(), allowed_routes) } } diff --git a/crates/workflow/src/data/requests.rs b/crates/workflow/src/data/requests.rs index aeb9df23..f9ede46a 100644 --- a/crates/workflow/src/data/requests.rs +++ b/crates/workflow/src/data/requests.rs @@ -1,15 +1,18 @@ #![deny(clippy::all, clippy::pedantic, clippy::nursery, warnings)] -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; +use ts_rs::TS; use utoipa::ToSchema; /// Request to create a new workflow -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct CreateWorkflowRequest { /// Workflow name pub name: String, /// Workflow description + #[serde(default)] pub description: Option, /// Workflow kind (consumer or provider) #[serde(rename = "kind")] @@ -17,8 +20,10 @@ pub struct CreateWorkflowRequest { /// Whether the workflow is enabled pub enabled: bool, /// Cron schedule for the workflow + #[serde(default)] pub schedule_cron: Option, /// Workflow configuration + #[ts(type = "unknown")] pub config: Value, /// Whether versioning is disabled #[serde(default)] @@ -26,11 +31,13 @@ pub struct CreateWorkflowRequest { } /// Request to update an existing workflow -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Serialize, Deserialize, ToSchema, TS)] +#[ts(export)] pub struct UpdateWorkflowRequest { /// Workflow name pub name: String, /// Workflow description + #[serde(default)] pub description: Option, /// Workflow kind (consumer or provider) #[serde(rename = "kind")] @@ -38,8 +45,10 @@ pub struct UpdateWorkflowRequest { /// Whether the workflow is enabled pub enabled: bool, /// Cron schedule for the workflow + #[serde(default)] pub schedule_cron: Option, /// Workflow configuration + #[ts(type = "unknown")] pub config: Value, /// Whether versioning is disabled #[serde(default)] diff --git a/crates/workflow/src/dsl/on_complete.rs b/crates/workflow/src/dsl/on_complete.rs index 7f3b441f..44ec1857 100644 --- a/crates/workflow/src/dsl/on_complete.rs +++ b/crates/workflow/src/dsl/on_complete.rs @@ -31,6 +31,7 @@ pub struct PostRunSendEmail { /// Recipients (only `const_string` — no field refs in post-run context) pub to: Vec, /// Optional CC + #[serde(default)] pub cc: Option>, /// When to fire this action #[serde(default)] diff --git a/crates/workflow/src/dsl/transform.rs b/crates/workflow/src/dsl/transform.rs index 42c76b55..bc551225 100644 --- a/crates/workflow/src/dsl/transform.rs +++ b/crates/workflow/src/dsl/transform.rs @@ -161,6 +161,7 @@ pub struct SendEmailTransform { /// Recipients: field refs or constant email addresses pub to: Vec, /// Optional CC recipients + #[serde(default)] pub cc: Option>, /// Normalized field to store send result (`"queued"`, `"mail_not_configured"`, or error) pub target_status: String, diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 00000000..57100064 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,26 @@ +# Example Deployments + +> **⚠️ Example only — not production-ready as-is.** These files are starting points for your own deployment. No warranty or guarantee is provided, express or implied. You are responsible for hardening security (secrets management, network policies, TLS, backup strategy, resource tuning, RBAC), and for adapting these examples to your infrastructure. Review every manifest before applying it to any environment. + +This directory contains two runnable example deployments of RDataCore: + +| Example | Location | Use when | +|---------|----------|----------| +| **Docker Compose** | [`docker-compose/`](./docker-compose/) | Single-host deployments, evaluation, small self-hosted setups | +| **Kubernetes + Longhorn** | [`kubernetes/`](./kubernetes/) | Cluster deployments with Longhorn persistent storage | + +Both examples deploy the full backend stack: the API server, workflow worker, maintenance worker, PostgreSQL, and Redis. Neither example includes the admin frontend container, an SMTP server, or development tooling — those are expected to be provided externally in a real environment. + +## What's not included (deliberately) + +- Helm charts or Kustomize overlays — raw YAML is easier to read and adapt +- SealedSecrets / external-secrets integration — use your org's secret management +- cert-manager manifests — the Ingress has a TLS placeholder you can wire to your own issuer +- NetworkPolicies — scope them to your cluster's network model +- Backup/restore tooling for PostgreSQL — use your preferred backup solution +- Observability stack (metrics, logs, tracing) + +## Related docs + +- [Root README](../../README.md) — quick start with the in-repo development Docker Compose +- [DEVELOPMENT.md](../DEVELOPMENT.md) — local development environment setup diff --git a/docs/examples/docker-compose/README.md b/docs/examples/docker-compose/README.md new file mode 100644 index 00000000..10f05b4a --- /dev/null +++ b/docs/examples/docker-compose/README.md @@ -0,0 +1,79 @@ +# Docker Compose — Production-Flavored Example + +> **⚠️ Example only — not production-ready as-is.** No warranty or guarantee. Review every line before deploying. You are responsible for secrets management, TLS termination, backups, and resource sizing. + +This example deploys the full RDataCore backend (API, worker, maintenance, PostgreSQL, Redis) on a single host using pre-built images from GitHub Container Registry. + +It differs from the in-repo development `compose.yaml` at the repo root: + +- Uses pre-built images (no local build context) +- No `dinghy` / `VIRTUAL_HOST` routing — publishes on `localhost:8080` +- Omits dev-only services (mailpit, playwright, frontend node container, nginx-proxy, postgres_test) +- `restart: unless-stopped` on every service +- Secrets loaded from a user-created `.env` + +## Prerequisites + +- Docker Engine 20.10+ with Compose v2 plugin +- A valid RDataCore license key + +## Setup + +```bash +cd docs/examples/docker-compose +cp env.example .env +# Edit .env and fill in POSTGRES_PASSWORD, JWT_SECRET, LICENSE_KEY +``` + +> The template is named `env.example` (no leading dot) because dotfile templates are noisy in tooling. Your actual secrets file is still `.env`. + +Generate strong secrets, for example: + +```bash +openssl rand -base64 32 # Use for POSTGRES_PASSWORD and JWT_SECRET +``` + +## Run migrations (one-shot) + +The `migrate` service is gated behind a Compose profile so it only runs when you ask for it: + +```bash +docker compose --profile migrate run --rm migrate +``` + +This executes `/usr/local/bin/run_migrations` against the Postgres database and exits. Re-run this step after every upgrade that ships new migrations. + +## Start the stack + +```bash +docker compose up -d +``` + +The API becomes available at `http://localhost:8080`. Check health: + +```bash +curl -sf http://localhost:8080/api/v1/health | jq +``` + +## Upgrade + +```bash +docker compose pull +docker compose --profile migrate run --rm migrate +docker compose up -d +``` + +## Stop / tear down + +```bash +docker compose down # keeps volumes +docker compose down -v # also drops postgres_data and redis_data — DESTRUCTIVE +``` + +## Caveats + +- `:latest` image tag drifts. For any real deployment pin to a specific version tag. +- The API is exposed on `localhost:8080` without TLS. Put it behind a reverse proxy (Caddy, nginx, Traefik) terminating TLS. +- `CORS_ORIGINS=*` is permissive — restrict this to your frontend origin(s). +- No external SMTP is included. Set `SYSTEM_SMTP_DSN` / `WORKFLOW_SMTP_DSN` in `.env` to enable email. +- Postgres data lives in a named Docker volume. Back it up with your tool of choice (`pg_dump`, `pgBackRest`, Restic, etc.). diff --git a/docs/examples/docker-compose/compose.yaml b/docs/examples/docker-compose/compose.yaml new file mode 100644 index 00000000..6781c933 --- /dev/null +++ b/docs/examples/docker-compose/compose.yaml @@ -0,0 +1,126 @@ +# RDataCore — Docker Compose example (production-flavoured starter) +# See ./README.md for the walkthrough and caveats. + +services: + postgres: + image: postgres:18-alpine + restart: unless-stopped + environment: + POSTGRES_USER: rdatacore + POSTGRES_DB: rdata + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U rdatacore -d rdata" ] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:8-alpine + restart: unless-stopped + command: [ "redis-server", "--appendonly", "yes" ] + volumes: + - redis_data:/data + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 5s + timeout: 5s + retries: 5 + + migrate: + image: ghcr.io/bentbr/r-data-core:latest + restart: "no" + depends_on: + postgres: + condition: service_healthy + entrypoint: [ "/usr/local/bin/run_migrations" ] + environment: + DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}@postgres:5432/rdata + profiles: [ "migrate" ] + + core: + image: ghcr.io/bentbr/r-data-core:latest + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + APP_ENV: production + API_HOST: 0.0.0.0 + API_PORT: 8888 + API_ENABLE_DOCS: "false" + RUST_LOG: info + LOG_LEVEL: info + CACHE_ENABLED: "true" + CACHE_TTL: 300 + CORS_ORIGINS: "*" + DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + REDIS_URL: redis://redis:6379 + JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set in .env} + JWT_EXPIRATION: 86400 + LICENSE_KEY: ${LICENSE_KEY:?LICENSE_KEY must be set in .env} + QUEUE_FETCH_KEY: queue:workflows:fetch + QUEUE_PROCESS_KEY: queue:workflows:process + QUEUE_EMAIL_KEY: queue:email + SYSTEM_SMTP_DSN: ${SYSTEM_SMTP_DSN:-} + WORKFLOW_SMTP_DSN: ${WORKFLOW_SMTP_DSN:-} + FRONTEND_BASE_URL: ${FRONTEND_BASE_URL:-http://localhost:8080} + PASSWORD_RESET_THROTTLE_SECONDS: 60 + ports: + - "8080:8888" + + worker: + image: ghcr.io/bentbr/r-data-core-worker:latest + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + RUST_LOG: info + LOG_LEVEL: info + DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + WORKER_DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + REDIS_URL: redis://redis:6379 + QUEUE_FETCH_KEY: queue:workflows:fetch + QUEUE_PROCESS_KEY: queue:workflows:process + QUEUE_EMAIL_KEY: queue:email + SYSTEM_SMTP_DSN: ${SYSTEM_SMTP_DSN:-} + WORKFLOW_SMTP_DSN: ${WORKFLOW_SMTP_DSN:-} + FRONTEND_BASE_URL: ${FRONTEND_BASE_URL:-http://localhost:8080} + PASSWORD_RESET_THROTTLE_SECONDS: 60 + + maintenance: + image: ghcr.io/bentbr/r-data-core-maintenance:latest + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + RUST_LOG: info + LOG_LEVEL: info + DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + WORKER_DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + MAINTENANCE_DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + REDIS_URL: redis://redis:6379 + VERSION_PURGER_CRON: "0 */5 * * * *" + REFRESH_TOKEN_CLEANUP_CRON: "10 */5 * * * *" + WORKFLOW_RUN_LOGS_PURGER_CRON: "20 */5 * * * *" + SYSTEM_LOGS_PURGER_CRON: "30 */5 * * * *" + SYSTEM_LOGS_RETENTION_DAYS: 90 + QUEUE_EMAIL_KEY: queue:email + SYSTEM_SMTP_DSN: ${SYSTEM_SMTP_DSN:-} + WORKFLOW_SMTP_DSN: ${WORKFLOW_SMTP_DSN:-} + FRONTEND_BASE_URL: ${FRONTEND_BASE_URL:-http://localhost:8080} + PASSWORD_RESET_THROTTLE_SECONDS: 60 + +volumes: + postgres_data: + redis_data: diff --git a/docs/examples/docker-compose/env.example b/docs/examples/docker-compose/env.example new file mode 100644 index 00000000..40fbc1ca --- /dev/null +++ b/docs/examples/docker-compose/env.example @@ -0,0 +1,21 @@ +# ───────────────────────────────────────────────────────────── +# RDataCore Docker Compose example — required configuration +# ───────────────────────────────────────────────────────────── +# Copy this file to `.env` and fill in real values: +# cp env.example .env +# Never commit your real `.env`. + +# ── Required secrets ──────────────────────────────────────── +POSTGRES_PASSWORD=change-me-to-a-long-random-string +JWT_SECRET=change-me-to-a-long-random-string-at-least-32-chars +LICENSE_KEY=paste-your-license-jwt-here + +# ── Optional: SMTP for system and workflow emails ─────────── +# Format: smtp://host:port?tls=true&from=addr&from_name=Name +# Omit both to disable email features. +SYSTEM_SMTP_DSN= +WORKFLOW_SMTP_DSN= + +# ── Optional: public-facing URLs ──────────────────────────── +# Used in password-reset emails and OpenAPI examples. +FRONTEND_BASE_URL=http://localhost:8080 diff --git a/docs/examples/kubernetes/00-namespace.yaml b/docs/examples/kubernetes/00-namespace.yaml new file mode 100644 index 00000000..887a90e7 --- /dev/null +++ b/docs/examples/kubernetes/00-namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: rdatacore + labels: + app.kubernetes.io/name: rdatacore + app.kubernetes.io/part-of: rdatacore diff --git a/docs/examples/kubernetes/01-configmap.yaml b/docs/examples/kubernetes/01-configmap.yaml new file mode 100644 index 00000000..e088bba0 --- /dev/null +++ b/docs/examples/kubernetes/01-configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: rdatacore-config + namespace: rdatacore +data: + APP_ENV: "production" + API_HOST: "0.0.0.0" + API_PORT: "8888" + API_ENABLE_DOCS: "false" + RUST_LOG: "info" + LOG_LEVEL: "info" + CACHE_ENABLED: "true" + CACHE_TTL: "300" # 5 minutes + CORS_ORIGINS: "*" + JWT_EXPIRATION: "86400" # 24 hours + QUEUE_FETCH_KEY: "queue:workflows:fetch" + QUEUE_PROCESS_KEY: "queue:workflows:process" + QUEUE_EMAIL_KEY: "queue:email" + PASSWORD_RESET_THROTTLE_SECONDS: "60" + # Maintenance cron expressions (staggered every 5 min) + VERSION_PURGER_CRON: "0 */5 * * * *" + REFRESH_TOKEN_CLEANUP_CRON: "10 */5 * * * *" + WORKFLOW_RUN_LOGS_PURGER_CRON: "20 */5 * * * *" + SYSTEM_LOGS_PURGER_CRON: "30 */5 * * * *" + SYSTEM_LOGS_RETENTION_DAYS: "90" + # Public-facing URL — adjust to match your Ingress host + FRONTEND_BASE_URL: "https://api.example.com" diff --git a/docs/examples/kubernetes/02-secret.yaml.example b/docs/examples/kubernetes/02-secret.yaml.example new file mode 100644 index 00000000..e6dcb161 --- /dev/null +++ b/docs/examples/kubernetes/02-secret.yaml.example @@ -0,0 +1,29 @@ +# Template — copy to 02-secret.yaml and fill in real values, +# or create the Secret imperatively: +# +# kubectl -n rdatacore create secret generic rdatacore-secrets \ +# --from-literal=POSTGRES_PASSWORD='...' \ +# --from-literal=JWT_SECRET='...' \ +# --from-literal=LICENSE_KEY='...' \ +# --from-literal=DATABASE_URL='postgres://rdatacore:PASSWORD@postgres:5432/rdata' \ +# --from-literal=REDIS_URL='redis://redis:6379' \ +# --from-literal=SYSTEM_SMTP_DSN='' \ +# --from-literal=WORKFLOW_SMTP_DSN='' +# +# Do NOT commit the filled-in version. +apiVersion: v1 +kind: Secret +metadata: + name: rdatacore-secrets + namespace: rdatacore +type: Opaque +stringData: + POSTGRES_PASSWORD: "change-me-long-random-string" + JWT_SECRET: "change-me-long-random-string-at-least-32-chars" + LICENSE_KEY: "paste-your-license-jwt-here" + # DATABASE_URL embeds POSTGRES_PASSWORD; keep them in sync. + DATABASE_URL: "postgres://rdatacore:change-me-long-random-string@postgres:5432/rdata" + REDIS_URL: "redis://redis:6379" + # Leave SMTP DSNs empty to disable email features. + SYSTEM_SMTP_DSN: "" + WORKFLOW_SMTP_DSN: "" diff --git a/docs/examples/kubernetes/10-postgres.yaml b/docs/examples/kubernetes/10-postgres.yaml new file mode 100644 index 00000000..33b669f8 --- /dev/null +++ b/docs/examples/kubernetes/10-postgres.yaml @@ -0,0 +1,100 @@ +# Headless Service — governs the StatefulSet, provides stable per-pod DNS +# (postgres-0.postgres-headless.rdatacore.svc.cluster.local). +apiVersion: v1 +kind: Service +metadata: + name: postgres-headless + namespace: rdatacore + labels: + app: postgres +spec: + clusterIP: None + ports: + - name: postgres + port: 5432 + targetPort: 5432 + selector: + app: postgres +--- +# ClusterIP Service — stable virtual IP for clients (DATABASE_URL points here). +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: rdatacore + labels: + app: postgres +spec: + type: ClusterIP + ports: + - name: postgres + port: 5432 + targetPort: 5432 + selector: + app: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: rdatacore +spec: + serviceName: postgres-headless + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:18-alpine + ports: + - containerPort: 5432 + name: postgres + env: + - name: POSTGRES_USER + value: "rdatacore" + - name: POSTGRES_DB + value: "rdata" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: rdatacore-secrets + key: POSTGRES_PASSWORD + - name: PGDATA + value: "/var/lib/postgresql/data/pgdata" + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + readinessProbe: + exec: + command: [ "pg_isready", "-U", "rdatacore", "-d", "rdata" ] + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + livenessProbe: + exec: + command: [ "pg_isready", "-U", "rdatacore", "-d", "rdata" ] + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1000m" + memory: "2Gi" + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: longhorn + resources: + requests: + storage: 20Gi diff --git a/docs/examples/kubernetes/11-redis.yaml b/docs/examples/kubernetes/11-redis.yaml new file mode 100644 index 00000000..3e4afe8c --- /dev/null +++ b/docs/examples/kubernetes/11-redis.yaml @@ -0,0 +1,87 @@ +# Headless Service — governs the StatefulSet, provides stable per-pod DNS. +apiVersion: v1 +kind: Service +metadata: + name: redis-headless + namespace: rdatacore + labels: + app: redis +spec: + clusterIP: None + ports: + - name: redis + port: 6379 + targetPort: 6379 + selector: + app: redis +--- +# ClusterIP Service — stable virtual IP for clients (REDIS_URL points here). +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: rdatacore + labels: + app: redis +spec: + type: ClusterIP + ports: + - name: redis + port: 6379 + targetPort: 6379 + selector: + app: redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + namespace: rdatacore +spec: + serviceName: redis-headless + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:8-alpine + # Use `command` (not `args`) so this matches the Compose example exactly. + command: [ "redis-server", "--appendonly", "yes", "--dir", "/data" ] + ports: + - containerPort: 6379 + name: redis + volumeMounts: + - name: data + mountPath: /data + readinessProbe: + exec: + command: [ "redis-cli", "ping" ] + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + exec: + command: [ "redis-cli", "ping" ] + initialDelaySeconds: 15 + periodSeconds: 15 + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "1Gi" + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: longhorn + resources: + requests: + storage: 5Gi diff --git a/docs/examples/kubernetes/20-migrations-job.yaml b/docs/examples/kubernetes/20-migrations-job.yaml new file mode 100644 index 00000000..16296ff5 --- /dev/null +++ b/docs/examples/kubernetes/20-migrations-job.yaml @@ -0,0 +1,31 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: rdatacore-migrate + namespace: rdatacore +spec: + backoffLimit: 3 + ttlSecondsAfterFinished: 300 + template: + metadata: + labels: + app: rdatacore + component: migrate + spec: + restartPolicy: OnFailure + containers: + - name: migrate + image: ghcr.io/bentbr/r-data-core:latest + command: [ "/usr/local/bin/run_migrations" ] + envFrom: + - configMapRef: + name: rdatacore-config + - secretRef: + name: rdatacore-secrets + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" diff --git a/docs/examples/kubernetes/30-core.yaml b/docs/examples/kubernetes/30-core.yaml new file mode 100644 index 00000000..a71b9fbf --- /dev/null +++ b/docs/examples/kubernetes/30-core.yaml @@ -0,0 +1,107 @@ +apiVersion: v1 +kind: Service +metadata: + name: rdatacore-core + namespace: rdatacore + labels: + app: rdatacore + component: core +spec: + type: ClusterIP + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: rdatacore + component: core +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rdatacore-core + namespace: rdatacore + labels: + app: rdatacore + component: core +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app: rdatacore + component: core + template: + metadata: + labels: + app: rdatacore + component: core + spec: + containers: + - name: core + image: ghcr.io/bentbr/r-data-core:latest + ports: + - containerPort: 8888 + name: http + envFrom: + - configMapRef: + name: rdatacore-config + - secretRef: + name: rdatacore-secrets + readinessProbe: + httpGet: + path: /api/v1/health + port: 8888 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + livenessProbe: + httpGet: + path: /api/v1/health + port: 8888 + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2000m" + memory: "2Gi" +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: rdatacore-core + namespace: rdatacore +spec: + minAvailable: 1 + selector: + matchLabels: + app: rdatacore + component: core +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: rdatacore-core + namespace: rdatacore +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: rdatacore-core + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 diff --git a/docs/examples/kubernetes/31-worker.yaml b/docs/examples/kubernetes/31-worker.yaml new file mode 100644 index 00000000..d625eb95 --- /dev/null +++ b/docs/examples/kubernetes/31-worker.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rdatacore-worker + namespace: rdatacore + labels: + app: rdatacore + component: worker +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app: rdatacore + component: worker + template: + metadata: + labels: + app: rdatacore + component: worker + spec: + containers: + - name: worker + image: ghcr.io/bentbr/r-data-core-worker:latest + envFrom: + - configMapRef: + name: rdatacore-config + - secretRef: + name: rdatacore-secrets + env: + # Worker reads WORKER_DATABASE_URL; mirror DATABASE_URL from the Secret. + - name: WORKER_DATABASE_URL + valueFrom: + secretKeyRef: + name: rdatacore-secrets + key: DATABASE_URL + # Worker is not an HTTP service — liveness check confirms the + # Rust process is still PID 1. Not a deep health probe, but + # enough for kubelet to restart the pod if the binary crashes + # into an unresponsive state without exiting. + livenessProbe: + exec: + command: [ "pgrep", "r_data_core" ] + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1000m" + memory: "1Gi" diff --git a/docs/examples/kubernetes/32-maintenance.yaml b/docs/examples/kubernetes/32-maintenance.yaml new file mode 100644 index 00000000..82b89b03 --- /dev/null +++ b/docs/examples/kubernetes/32-maintenance.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rdatacore-maintenance + namespace: rdatacore + labels: + app: rdatacore + component: maintenance +spec: + replicas: 1 + # Recreate strategy: maintenance owns internal cron schedules. + # Rolling update would transiently run two replicas and double-fire tasks. + strategy: + type: Recreate + selector: + matchLabels: + app: rdatacore + component: maintenance + template: + metadata: + labels: + app: rdatacore + component: maintenance + spec: + containers: + - name: maintenance + image: ghcr.io/bentbr/r-data-core-maintenance:latest + envFrom: + - configMapRef: + name: rdatacore-config + - secretRef: + name: rdatacore-secrets + env: + # Maintenance uses its own DB URL var; mirror DATABASE_URL from the Secret. + - name: MAINTENANCE_DATABASE_URL + valueFrom: + secretKeyRef: + name: rdatacore-secrets + key: DATABASE_URL + - name: WORKER_DATABASE_URL + valueFrom: + secretKeyRef: + name: rdatacore-secrets + key: DATABASE_URL + # Maintenance is not an HTTP service — liveness check confirms + # the Rust process is still running. Same rationale as the + # worker Deployment. + livenessProbe: + exec: + command: [ "pgrep", "r_data_core" ] + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" diff --git a/docs/examples/kubernetes/40-ingress.yaml b/docs/examples/kubernetes/40-ingress.yaml new file mode 100644 index 00000000..0d5f989d --- /dev/null +++ b/docs/examples/kubernetes/40-ingress.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rdatacore + namespace: rdatacore + annotations: + # Uncomment and adjust for cert-manager: + # cert-manager.io/cluster-issuer: letsencrypt-prod + # ingress-nginx-specific — other controllers (Traefik, HAProxy, AWS LB) + # expose body-size limits under different annotation keys. Translate as needed. + nginx.ingress.kubernetes.io/proxy-body-size: "16m" +spec: + ingressClassName: nginx + tls: + - hosts: + - api.example.com + secretName: rdatacore-tls # managed by cert-manager or populated manually + rules: + - host: api.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: rdatacore-core + port: + number: 8888 diff --git a/docs/examples/kubernetes/README.md b/docs/examples/kubernetes/README.md new file mode 100644 index 00000000..a5033956 --- /dev/null +++ b/docs/examples/kubernetes/README.md @@ -0,0 +1,141 @@ +# Kubernetes + Longhorn — Production-Flavored Example + +> **⚠️ Example only — not production-ready as-is.** No warranty or guarantee. Review every manifest. You are responsible for secrets management, NetworkPolicies, TLS, backup, resource tuning, and RBAC. + +This example deploys the full RDataCore backend on Kubernetes with Longhorn-backed persistent storage for PostgreSQL and Redis. + +## Prerequisites + +- Kubernetes 1.27+ +- [Longhorn](https://longhorn.io) installed, with a `StorageClass` named `longhorn` available +- An Ingress controller (the example uses `ingressClassName: nginx`) +- A working `kubectl` context with permission to create Namespaces, StatefulSets, Deployments, Jobs, Services, Secrets, ConfigMaps, Ingresses, PodDisruptionBudgets, and HorizontalPodAutoscalers +- Optional: [cert-manager](https://cert-manager.io) for automatic TLS (the Ingress annotation is commented out by default) +- A valid RDataCore license key + +Verify Longhorn is your storage class: + +```bash +kubectl get storageclass +# You should see "longhorn" listed +``` + +If your storage class has a different name, either edit the PVCs in `10-postgres.yaml` and `11-redis.yaml`, or rename your StorageClass. + +## What gets deployed + +| Manifest | Kind | Notes | +|----------|------|-------| +| `00-namespace.yaml` | Namespace | `rdatacore` | +| `01-configmap.yaml` | ConfigMap | Non-secret env (cron schedules, log levels, etc.) | +| `02-secret.yaml.example` | Secret (template) | Passwords, JWT secret, license key, DSNs | +| `10-postgres.yaml` | StatefulSet + Service | Postgres 18 with Longhorn PVC (20 GiB) | +| `11-redis.yaml` | StatefulSet + Service | Redis 8 with Longhorn PVC (5 GiB), appendonly | +| `20-migrations-job.yaml` | Job | Runs `run_migrations` once | +| `30-core.yaml` | Deployment + Service + PDB + HPA | API, 2 replicas, HPA 2–5 on 70% CPU | +| `31-worker.yaml` | Deployment | Workflow worker, 1 replica (stateless — scale as needed) | +| `32-maintenance.yaml` | Deployment | Cron singleton — `strategy: Recreate` | +| `40-ingress.yaml` | Ingress | nginx-class, TLS placeholder | + +## Setup + +**1. Create the Secret from the template:** + +```bash +cp 02-secret.yaml.example 02-secret.yaml +# Edit 02-secret.yaml: replace the placeholder values with real secrets. +``` + +> **⚠️ Password coupling:** `DATABASE_URL` in the Secret embeds `POSTGRES_PASSWORD` directly. Any time you rotate the Postgres password you must update **both** `POSTGRES_PASSWORD` and the password portion of `DATABASE_URL` in the same Secret, then re-apply and roll the core/worker/maintenance Deployments. Forgetting one leaves the stack in a broken state. If you dislike this coupling, split `DATABASE_URL` into components (host, port, db, user, password) and compose it at runtime in your own wrapper. + +Alternatively, create the Secret imperatively — see the header of `02-secret.yaml.example` for the `kubectl create secret` command. If you use the imperative route, you do NOT apply `02-secret.yaml` in the next step. + +**Generate strong secrets:** + +```bash +openssl rand -base64 32 +``` + +**2. Adjust `01-configmap.yaml` and `40-ingress.yaml`:** + +- Set `FRONTEND_BASE_URL` in `01-configmap.yaml` to your public API URL (e.g. `https://api.example.com`). +- Change `api.example.com` in `40-ingress.yaml` to your real hostname. +- If you use cert-manager, uncomment the `cert-manager.io/cluster-issuer` annotation and point it at your ClusterIssuer. + +## Apply order + +Apply manifests in filename order so dependencies are satisfied. Wait for Postgres and Redis to become Ready before applying the migrations Job. + +```bash +kubectl apply -f 00-namespace.yaml +kubectl apply -f 01-configmap.yaml +kubectl apply -f 02-secret.yaml # or use the imperative create-secret command + +kubectl apply -f 10-postgres.yaml +kubectl apply -f 11-redis.yaml + +# Wait for StatefulSets to be Ready +kubectl -n rdatacore rollout status sts/postgres --timeout=5m +kubectl -n rdatacore rollout status sts/redis --timeout=5m + +kubectl apply -f 20-migrations-job.yaml +kubectl -n rdatacore wait --for=condition=complete job/rdatacore-migrate --timeout=5m + +kubectl apply -f 30-core.yaml +kubectl apply -f 31-worker.yaml +kubectl apply -f 32-maintenance.yaml +kubectl apply -f 40-ingress.yaml +``` + +## Verify + +```bash +kubectl -n rdatacore get pods +kubectl -n rdatacore logs deploy/rdatacore-core +kubectl -n rdatacore exec -it sts/postgres -- psql -U rdatacore -d rdata -c '\dt' +``` + +Hit the health endpoint from inside the cluster (bypasses the Ingress): + +```bash +kubectl -n rdatacore run curl --rm -it --image=curlimages/curl --restart=Never -- \ + curl -sf http://rdatacore-core.rdatacore.svc.cluster.local:8888/api/v1/health +``` + +Once your DNS points at the Ingress and TLS is configured: + +```bash +curl -sf https://api.example.com/api/v1/health | jq +``` + +## Upgrade + +```bash +# Pull new images (forces a rollout if you use :latest; otherwise bump the tag in each manifest) +kubectl -n rdatacore rollout restart deploy/rdatacore-core deploy/rdatacore-worker deploy/rdatacore-maintenance + +# Re-run migrations +kubectl -n rdatacore delete job rdatacore-migrate --ignore-not-found +kubectl apply -f 20-migrations-job.yaml +kubectl -n rdatacore wait --for=condition=complete job/rdatacore-migrate --timeout=5m +``` + +## Tear down + +```bash +kubectl delete namespace rdatacore +``` + +This deletes all Deployments/StatefulSets/Services. Longhorn PVs are governed by their reclaim policy — verify they are removed or retained as you expect. + +## Caveats + +- **`:latest` tags drift.** For any real deployment, pin each image to a specific version tag. +- **Single-replica stateful services.** Postgres and Redis run as 1-replica StatefulSets here. For HA, use a managed Postgres service and Redis Sentinel/Cluster — or a Postgres operator such as CloudNativePG. +- **Longhorn storage class name** must be `longhorn` as shipped, or you must edit the PVC specs. +- **CORS_ORIGINS=*** is permissive. Override in `01-configmap.yaml` to restrict to your frontend origins. +- **No NetworkPolicies, no RBAC lockdown.** Add them to suit your cluster's security model. +- **No backup** is configured for the Postgres PVC. Use `pgBackRest`, Velero, or another tool as appropriate. +- **Resource limits** are starting points. Tune based on your workload and node sizing. +- **Maintenance is a Deployment, not a CronJob**, because the maintenance binary owns its schedule. `strategy: Recreate` prevents two replicas running simultaneously during rollouts. +- **Only the core Deployment has a PodDisruptionBudget.** Worker and maintenance run as single replicas (stateless queue consumer and cron singleton respectively), so a PDB would just block node drains. Scale worker horizontally if you need resilience there, and add a PDB at that point. diff --git a/fe/e2e/page-objects/components/dialog.component.ts b/fe/e2e/page-objects/components/dialog.component.ts deleted file mode 100644 index cc783a9f..00000000 --- a/fe/e2e/page-objects/components/dialog.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type Page, expect } from '@playwright/test' - -export class DialogComponent { - constructor(private readonly page: Page) {} - - private get dialog() { - return this.page.locator('.v-dialog--active, .v-overlay--active .v-card') - } - - async expectOpen(): Promise { - await expect(this.dialog).toBeVisible() - } - - async close(): Promise { - // Try the close button first, then ESC - const closeBtn = this.dialog.getByRole('button', { name: /close|cancel/i }) - if (await closeBtn.isVisible()) { - await closeBtn.click() - } else { - await this.page.keyboard.press('Escape') - } - await expect(this.dialog).not.toBeVisible() - } - - async confirm(): Promise { - const confirmBtn = this.dialog.getByRole('button', { name: /confirm|ok|yes|save|submit/i }) - await confirmBtn.click() - } -} diff --git a/fe/eslint.config.js b/fe/eslint.config.js index 8e50467f..f0a68c69 100644 --- a/fe/eslint.config.js +++ b/fe/eslint.config.js @@ -60,7 +60,7 @@ export default [ ecmaVersion: 'latest', sourceType: 'module', extraFileExtensions: ['.vue'], - project: true, + projectService: true, tsconfigRootDir: import.meta.dirname, }, globals: { @@ -110,7 +110,7 @@ export default [ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', - project: true, + projectService: true, tsconfigRootDir: import.meta.dirname, }, }, @@ -134,7 +134,7 @@ export default [ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', - project: true, + projectService: true, tsconfigRootDir: import.meta.dirname, }, }, @@ -156,7 +156,7 @@ export default [ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', - project: true, + projectService: true, tsconfigRootDir: import.meta.dirname, }, }, diff --git a/fe/knip.json b/fe/knip.json new file mode 100644 index 00000000..2dc2a289 --- /dev/null +++ b/fe/knip.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "entry": [ + "src/env-check.ts", + "src/router/index.ts", + "src/api/typed-client.ts", + "src/api/clients/index.ts", + "src/types/schemas/index.ts", + "scripts/**/*.{js,ts}", + "src/**/*.test.{ts,vue}" + ], + "project": ["src/**/*.{ts,vue}", "e2e/**/*.ts"], + "paths": { + "@/*": ["./src/*"] + }, + "rules": { + "files": "error", + "dependencies": "error", + "devDependencies": "error", + "exports": "error", + "types": "error", + "nsExports": "error", + "nsTypes": "error", + "enumMembers": "error", + "duplicates": "error", + "unlisted": "error", + "binaries": "off", + "unresolved": "error" + }, + "ignoreDependencies": ["@vueuse/core", "@vitest/coverage-v8"], + "ignore": [ + "src/design-system/**", + "src/types/generated/HealthData.ts", + "src/types/generated/DslOptionsAndExamplesResponse.ts", + "src/types/generated/validation.ts" + ], + "ignoreExportsUsedInFile": true +} diff --git a/fe/package-lock.json b/fe/package-lock.json index 71a06d15..fa481677 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -1,12 +1,12 @@ { "name": "r-data-core-admin", - "version": "0.4.8", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "r-data-core-admin", - "version": "0.4.8", + "version": "0.4.9", "dependencies": { "@vueuse/core": "^10.9.0", "lucide-vue-next": "^0.556.0", @@ -31,6 +31,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-vue": "^9.27.0", "jsdom": "^26.1.0", + "knip": "^6.4.1", "prettier": "^3.2.5", "sharp": "^0.33.5", "typescript": "~5.4.3", @@ -228,12 +229,38 @@ "node": ">=18" } }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", "dev": true, + "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1292,21 +1319,711 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.121.0.tgz", + "integrity": "sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.121.0.tgz", + "integrity": "sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.121.0.tgz", + "integrity": "sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.121.0.tgz", + "integrity": "sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.121.0.tgz", + "integrity": "sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.121.0.tgz", + "integrity": "sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.121.0.tgz", + "integrity": "sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.121.0.tgz", + "integrity": "sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.121.0.tgz", + "integrity": "sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.121.0.tgz", + "integrity": "sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.121.0.tgz", + "integrity": "sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.121.0.tgz", + "integrity": "sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.121.0.tgz", + "integrity": "sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.121.0.tgz", + "integrity": "sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.121.0.tgz", + "integrity": "sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.121.0.tgz", + "integrity": "sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.121.0.tgz", + "integrity": "sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.121.0.tgz", + "integrity": "sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.121.0.tgz", + "integrity": "sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.121.0.tgz", + "integrity": "sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.121.0.tgz", + "integrity": "sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", + "integrity": "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.19.1.tgz", + "integrity": "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", + "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.19.1.tgz", + "integrity": "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.19.1.tgz", + "integrity": "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.19.1.tgz", + "integrity": "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.19.1.tgz", + "integrity": "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.19.1.tgz", + "integrity": "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.19.1.tgz", + "integrity": "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.19.1.tgz", + "integrity": "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.19.1.tgz", + "integrity": "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.19.1.tgz", + "integrity": "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.19.1.tgz", + "integrity": "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.19.1.tgz", + "integrity": "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.19.1.tgz", + "integrity": "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.19.1.tgz", + "integrity": "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.19.1.tgz", + "integrity": "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.19.1.tgz", + "integrity": "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.19.1.tgz", + "integrity": "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.19.1.tgz", + "integrity": "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -1702,6 +2419,17 @@ "win32" ] }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -2512,6 +3240,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3161,6 +3902,36 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3173,6 +3944,26 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3208,6 +3999,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3260,6 +4064,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3274,6 +4094,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -3474,6 +4307,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3551,6 +4394,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-beautify": { "version": "1.15.4", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", @@ -3674,6 +4527,60 @@ "json-buffer": "3.0.1" } }, + "node_modules/knip": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.4.1.tgz", + "integrity": "sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "@nodelib/fs.walk": "^1.2.3", + "fast-glob": "^3.3.3", + "formatly": "^0.3.0", + "get-tsconfig": "4.13.7", + "jiti": "^2.6.0", + "minimist": "^1.2.8", + "oxc-parser": "^0.121.0", + "oxc-resolver": "^11.19.1", + "picocolors": "^1.1.1", + "picomatch": "^4.0.1", + "smol-toml": "^1.6.1", + "strip-json-comments": "5.0.3", + "unbash": "^2.2.0", + "yaml": "^2.8.2", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3769,6 +4676,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -3785,6 +4729,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3888,6 +4842,76 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-parser": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.121.0.tgz", + "integrity": "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.121.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.121.0", + "@oxc-parser/binding-android-arm64": "0.121.0", + "@oxc-parser/binding-darwin-arm64": "0.121.0", + "@oxc-parser/binding-darwin-x64": "0.121.0", + "@oxc-parser/binding-freebsd-x64": "0.121.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.121.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.121.0", + "@oxc-parser/binding-linux-arm64-musl": "0.121.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.121.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.121.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.121.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.121.0", + "@oxc-parser/binding-linux-x64-gnu": "0.121.0", + "@oxc-parser/binding-linux-x64-musl": "0.121.0", + "@oxc-parser/binding-openharmony-arm64": "0.121.0", + "@oxc-parser/binding-wasm32-wasi": "0.121.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.121.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.121.0", + "@oxc-parser/binding-win32-x64-msvc": "0.121.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", + "integrity": "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.19.1", + "@oxc-resolver/binding-android-arm64": "11.19.1", + "@oxc-resolver/binding-darwin-arm64": "11.19.1", + "@oxc-resolver/binding-darwin-x64": "11.19.1", + "@oxc-resolver/binding-freebsd-x64": "11.19.1", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", + "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", + "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", + "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-x64-musl": "11.19.1", + "@oxc-resolver/binding-openharmony-arm64": "11.19.1", + "@oxc-resolver/binding-wasm32-wasi": "11.19.1", + "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", + "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", + "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4180,6 +5204,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4189,6 +5234,27 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -4240,6 +5306,30 @@ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4371,6 +5461,19 @@ "node": ">=18" } }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4631,6 +5734,19 @@ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4720,6 +5836,16 @@ "node": ">=14.17" } }, + "node_modules/unbash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-2.2.0.tgz", + "integrity": "sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -5130,6 +6256,16 @@ "node": ">=18" } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -5337,6 +6473,22 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/fe/package.json b/fe/package.json index d42f687f..3fcfe836 100644 --- a/fe/package.json +++ b/fe/package.json @@ -20,7 +20,8 @@ "test:e2e:debug": "npx playwright test --config=e2e/playwright.config.ts --debug", "test:e2e:report": "npx playwright show-report e2e/playwright-report --host 0.0.0.0 --port 9323", "lint:e2e": "eslint e2e/", - "generate:favicons": "node scripts/generate-favicons.js" + "generate:favicons": "node scripts/generate-favicons.js", + "knip": "knip" }, "dependencies": { "@vueuse/core": "^10.9.0", @@ -32,9 +33,8 @@ "zod": "^4.0.13" }, "devDependencies": { - "@playwright/test": "^1.58.2", - "sharp": "^0.33.5", "@eslint/js": "^9.9.0", + "@playwright/test": "^1.58.2", "@types/node": "^20.11.30", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", @@ -47,7 +47,9 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-vue": "^9.27.0", "jsdom": "^26.1.0", + "knip": "^6.4.1", "prettier": "^3.2.5", + "sharp": "^0.33.5", "typescript": "~5.4.3", "vite": "^6.3.5", "vite-plugin-vuetify": "^2.1.2", diff --git a/fe/src/api/clients/api-keys.test.ts b/fe/src/api/clients/api-keys.test.ts index 976f847f..f3b5ec7d 100644 --- a/fe/src/api/clients/api-keys.test.ts +++ b/fe/src/api/clients/api-keys.test.ts @@ -59,7 +59,12 @@ describe('ApiKeysClient', () => { json: async () => mockResponse, }) - const result = await client.getApiKeys(1, 10) + const result = await client.getApiKeys({ + page: 1, + per_page: 10, + limit: null, + offset: null, + }) expect(result.data).toBeDefined() expect(Array.isArray(result.data)).toBe(true) @@ -93,7 +98,10 @@ describe('ApiKeysClient', () => { json: async () => mockResponse, }) - const result = await client.getApiKeys(1, 10, 'name', 'asc') + const result = await client.getApiKeys( + { page: 1, per_page: 10, limit: null, offset: null }, + { sort_by: 'name', sort_order: 'asc' } + ) expect(result.data).toBeDefined() expect(mockFetch).toHaveBeenCalledWith( @@ -126,7 +134,10 @@ describe('ApiKeysClient', () => { json: async () => mockResponse, }) - const result = await client.getApiKeys(1, 10, 'created_at', 'desc') + const result = await client.getApiKeys( + { page: 1, per_page: 10, limit: null, offset: null }, + { sort_by: 'created_at', sort_order: 'desc' } + ) expect(result.data).toBeDefined() expect(mockFetch).toHaveBeenCalledWith( @@ -159,7 +170,10 @@ describe('ApiKeysClient', () => { json: async () => mockResponse, }) - const result = await client.getApiKeys(1, 10, null, null) + const result = await client.getApiKeys( + { page: 1, per_page: 10, limit: null, offset: null }, + null + ) expect(result.data).toBeDefined() const url = mockFetch.mock.calls[0][0] as string @@ -172,6 +186,8 @@ describe('ApiKeysClient', () => { it('should create API key', async () => { const request: CreateApiKeyRequest = { name: 'Test Key', + description: null, + expires_in_days: null, } const mockResponse = { diff --git a/fe/src/api/clients/api-keys.ts b/fe/src/api/clients/api-keys.ts index f428617d..c7b359fc 100644 --- a/fe/src/api/clients/api-keys.ts +++ b/fe/src/api/clients/api-keys.ts @@ -1,35 +1,19 @@ import type { ApiKeyResponse } from '@/types/generated/ApiKeyResponse' import type { ApiKeyCreatedResponse } from '@/types/generated/ApiKeyCreatedResponse' -import type { CreateApiKeyRequest, ReassignApiKeyRequest } from '@/types/schemas' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' +import type { CreateApiKeyRequest, ReassignApiKeyRequest, ResponseMeta } from '@/types/schemas' import { BaseTypedHttpClient } from './base' +import { buildListQueryString } from './query' export class ApiKeysClient extends BaseTypedHttpClient { async getApiKeys( - page = 1, - itemsPerPage = 10, - sortBy?: string | null, - sortOrder?: 'asc' | 'desc' | null - ): Promise<{ - data: ApiKeyResponse[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - }> { - let url = `/admin/api/v1/api-keys?page=${page}&per_page=${itemsPerPage}` - if (sortBy && sortOrder) { - url += `&sort_by=${sortBy}&sort_order=${sortOrder}` - } - return this.paginatedRequest(url) + pagination: PaginationQuery, + sorting?: SortingQuery | null + ): Promise<{ data: ApiKeyResponse[]; meta?: ResponseMeta }> { + return this.paginatedRequest( + `/admin/api/v1/api-keys${buildListQueryString(pagination, sorting)}` + ) } async createApiKey(data: CreateApiKeyRequest): Promise { diff --git a/fe/src/api/clients/auth.ts b/fe/src/api/clients/auth.ts index ea255b9a..4b0ec069 100644 --- a/fe/src/api/clients/auth.ts +++ b/fe/src/api/clients/auth.ts @@ -1,5 +1,6 @@ import type { AdminLoginResponse } from '@/types/generated/AdminLoginResponse' import type { RefreshTokenResponse } from '@/types/generated/RefreshTokenResponse' +import type { UserPermissionsResponse } from '@/types/generated/UserPermissionsResponse' import type { LoginRequest, RefreshTokenRequest, LogoutRequest } from '@/types/schemas' import { BaseTypedHttpClient } from './base' @@ -32,16 +33,8 @@ export class AuthClient extends BaseTypedHttpClient { }) } - async getUserPermissions(): Promise<{ - is_super_admin: boolean - permissions: string[] - allowed_routes: string[] - }> { - return this.request<{ - is_super_admin: boolean - permissions: string[] - allowed_routes: string[] - }>('/admin/api/v1/auth/permissions') + async getUserPermissions(): Promise { + return this.request('/admin/api/v1/auth/permissions') } async forgotPassword(email: string): Promise { diff --git a/fe/src/api/clients/base.ts b/fe/src/api/clients/base.ts index 10e72637..2fef5cbb 100644 --- a/fe/src/api/clients/base.ts +++ b/fe/src/api/clients/base.ts @@ -2,16 +2,16 @@ import { env, buildApiUrl } from '@/env-check' import { useAuthStore } from '@/stores/auth' import { getRefreshToken } from '@/utils/cookies' import { ValidationErrorResponseSchema } from '@/types/schemas' -import type { Meta } from '@/types/schemas' +import type { Status, ResponseMeta } from '@/types/schemas' import { ValidationError } from '../http-client' import { HttpError, extractNamespaceFromEndpoint, extractActionFromMethod } from '../errors' -// Define ApiResponse type for backward compatibility -export type ApiResponse = { - status: 'Success' | 'Error' +// Generic envelope around every BE response. Status/meta shapes come from generated bindings. +type ApiResponse = { + status: Status message: string data?: T | null - meta?: Meta | null // Backend may return null + meta?: ResponseMeta | null } /** @@ -322,22 +322,7 @@ export class BaseTypedHttpClient { protected async paginatedRequest( endpoint: string, options: RequestInit = {} - ): Promise<{ - data: T - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - }> { + ): Promise<{ data: T; meta?: ResponseMeta }> { const authToken = await this.ensureToken(endpoint) const config = this.buildConfig(authToken, options) @@ -465,22 +450,7 @@ export class BaseTypedHttpClient { } } - protected validatePaginatedResponse(rawData: unknown): { - data: T - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - } { + protected validatePaginatedResponse(rawData: unknown): { data: T; meta?: ResponseMeta } { if (this.enableLogging && this.devMode) { console.log('[API] Response:', rawData) } diff --git a/fe/src/api/clients/dsl.ts b/fe/src/api/clients/dsl.ts index 61a37e4f..8c7540b2 100644 --- a/fe/src/api/clients/dsl.ts +++ b/fe/src/api/clients/dsl.ts @@ -1,5 +1,6 @@ import { BaseTypedHttpClient } from './base' import type { DslStep, DslOptionsResponse } from '@/types/schemas' +import type { DslValidateResponse } from '@/types/generated/DslValidateResponse' export class DslClient extends BaseTypedHttpClient { async getDslFromOptions(): Promise { @@ -14,9 +15,9 @@ export class DslClient extends BaseTypedHttpClient { return this.request('/admin/api/v1/dsl/transform/options') } - async validateDsl(steps: DslStep[]): Promise<{ valid: boolean }> { + async validateDsl(steps: DslStep[]): Promise { const request = { steps } - return this.request<{ valid: boolean }>('/admin/api/v1/dsl/validate', { + return this.request('/admin/api/v1/dsl/validate', { method: 'POST', body: JSON.stringify(request), }) diff --git a/fe/src/api/clients/email-templates.ts b/fe/src/api/clients/email-templates.ts index 9c1de647..bd1676a0 100644 --- a/fe/src/api/clients/email-templates.ts +++ b/fe/src/api/clients/email-templates.ts @@ -2,14 +2,14 @@ import { BaseTypedHttpClient } from './base' import type { EmailTemplateResponse } from '@/types/generated/EmailTemplateResponse' import type { CreateEmailTemplateRequest } from '@/types/generated/CreateEmailTemplateRequest' import type { UpdateEmailTemplateRequest } from '@/types/generated/UpdateEmailTemplateRequest' -import type { EmailTemplateType } from '@/types/generated/EmailTemplateType' +import type { EmailTemplateListQuery } from '@/types/generated/EmailTemplateListQuery' export type EmailTemplate = EmailTemplateResponse export type { CreateEmailTemplateRequest, UpdateEmailTemplateRequest } export class EmailTemplateClient extends BaseTypedHttpClient { - async list(type?: EmailTemplateType): Promise { - const params = type ? `?type=${type}` : '' + async list(query: EmailTemplateListQuery = { type: null }): Promise { + const params = query.type ? `?type=${query.type}` : '' return this.request(`/admin/api/v1/email-templates${params}`) } diff --git a/fe/src/api/clients/entities.ts b/fe/src/api/clients/entities.ts index 6ace8c3e..8a806dd7 100644 --- a/fe/src/api/clients/entities.ts +++ b/fe/src/api/clients/entities.ts @@ -3,35 +3,14 @@ import type { DynamicEntity, EntityResponse, UpdateEntityRequest, + ResponseMeta, } from '@/types/schemas' +import type { BrowseNode } from '@/types/generated/BrowseNode' +import type { VersionMeta } from '@/types/generated/VersionMeta' +import type { VersionPayload } from '@/types/generated/VersionPayload' import { BaseTypedHttpClient } from './base' -type PathEntry = { - kind: 'folder' | 'file' - name: string - path: string - entity_uuid?: string | null - entity_type?: string | null - has_children?: boolean | null - published: boolean -} - -type PaginatedPathResult = { - data: PathEntry[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } -} +type PaginatedBrowseResult = { data: BrowseNode[]; meta?: ResponseMeta } export class EntitiesClient extends BaseTypedHttpClient { async getEntities( @@ -39,22 +18,7 @@ export class EntitiesClient extends BaseTypedHttpClient { page = 1, itemsPerPage = 10, include?: string - ): Promise<{ - data: DynamicEntity[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - }> { + ): Promise<{ data: DynamicEntity[]; meta?: ResponseMeta }> { const includeParam = include ? `&include=${include}` : '' return this.paginatedRequest( `/api/v1/${entityType}?page=${page}&per_page=${itemsPerPage}${includeParam}` @@ -65,16 +29,16 @@ export class EntitiesClient extends BaseTypedHttpClient { path: string, limit = this.getDefaultPageSize(), offset = 0 - ): Promise { + ): Promise { const encoded = encodeURIComponent(path) - return this.paginatedRequest( + return this.paginatedRequest( `/api/v1/entities/by-path?path=${encoded}&limit=${limit}&offset=${offset}` ) } - async searchEntitiesByPath(searchTerm: string, limit = 10): Promise { + async searchEntitiesByPath(searchTerm: string, limit = 10): Promise { const encoded = encodeURIComponent(searchTerm) - return this.paginatedRequest( + return this.paginatedRequest( `/api/v1/entities/by-path?search=${encoded}&limit=${limit}` ) } @@ -152,42 +116,19 @@ export class EntitiesClient extends BaseTypedHttpClient { }) } - async listEntityVersions( - entityType: string, - uuid: string - ): Promise< - Array<{ - version_number: number - created_at: string - created_by?: string | null - created_by_name?: string | null - }> - > { - return this.request< - Array<{ - version_number: number - created_at: string - created_by?: string | null - created_by_name?: string | null - }> - >(`/api/v1/entities/${encodeURIComponent(entityType)}/${uuid}/versions`) + async listEntityVersions(entityType: string, uuid: string): Promise { + return this.request( + `/api/v1/entities/${encodeURIComponent(entityType)}/${uuid}/versions` + ) } async getEntityVersion( entityType: string, uuid: string, versionNumber: number - ): Promise<{ - version_number: number - created_at: string - created_by?: string | null - data: Record - }> { - return this.request<{ - version_number: number - created_at: string - created_by?: string | null - data: Record - }>(`/api/v1/entities/${encodeURIComponent(entityType)}/${uuid}/versions/${versionNumber}`) + ): Promise { + return this.request( + `/api/v1/entities/${encodeURIComponent(entityType)}/${uuid}/versions/${versionNumber}` + ) } } diff --git a/fe/src/api/clients/entity-definitions.test.ts b/fe/src/api/clients/entity-definitions.test.ts index 0c914469..0f1baaec 100644 --- a/fe/src/api/clients/entity-definitions.test.ts +++ b/fe/src/api/clients/entity-definitions.test.ts @@ -91,7 +91,12 @@ describe('EntityDefinitionsClient', () => { json: async () => mockResponse, }) - const result = await client.getEntityDefinitions(10, 0) + const result = await client.getEntityDefinitions({ + page: null, + per_page: null, + limit: 10, + offset: 0, + }) expect(result.data).toBeDefined() expect(Array.isArray(result.data)).toBe(true) @@ -126,7 +131,12 @@ describe('EntityDefinitionsClient', () => { json: async () => mockResponse, }) - const result = await client.getEntityDefinitions(20, 40) + const result = await client.getEntityDefinitions({ + page: null, + per_page: null, + limit: 20, + offset: 40, + }) expect(result.meta?.pagination?.has_previous).toBe(true) expect(result.meta?.pagination?.has_next).toBe(false) @@ -355,7 +365,7 @@ describe('EntityDefinitionsClient', () => { expect.stringContaining('/admin/api/v1/entity-definitions/apply-schema'), expect.objectContaining({ method: 'POST', - body: JSON.stringify({ uuid: undefined }), + body: JSON.stringify({ uuid: null }), }) ) }) diff --git a/fe/src/api/clients/entity-definitions.ts b/fe/src/api/clients/entity-definitions.ts index a146d098..5c7d9a85 100644 --- a/fe/src/api/clients/entity-definitions.ts +++ b/fe/src/api/clients/entity-definitions.ts @@ -2,32 +2,22 @@ import type { EntityDefinition, CreateEntityDefinitionRequest, UpdateEntityDefinitionRequest, + ResponseMeta, } from '@/types/schemas' +import type { ApplySchemaRequest } from '@/types/generated/ApplySchemaRequest' +import type { EntityDefinitionVersionMeta } from '@/types/generated/EntityDefinitionVersionMeta' +import type { EntityDefinitionVersionPayload } from '@/types/generated/EntityDefinitionVersionPayload' +import type { EntityFieldInfo } from '@/types/generated/EntityFieldInfo' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' import { BaseTypedHttpClient } from './base' +import { buildListQueryString } from './query' export class EntityDefinitionsClient extends BaseTypedHttpClient { async getEntityDefinitions( - limit?: number, - offset = 0 - ): Promise<{ - data: EntityDefinition[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - }> { - const pageSize = limit ?? this.getDefaultPageSize() + pagination: PaginationQuery + ): Promise<{ data: EntityDefinition[]; meta?: ResponseMeta }> { return this.paginatedRequest( - `/admin/api/v1/entity-definitions?limit=${pageSize}&offset=${offset}` + `/admin/api/v1/entity-definitions${buildListQueryString(pagination)}` ) } @@ -60,52 +50,31 @@ export class EntityDefinitionsClient extends BaseTypedHttpClient { async applyEntityDefinitionSchema(uuid?: string): Promise<{ message: string }> { const endpoint = '/admin/api/v1/entity-definitions/apply-schema' + const body: ApplySchemaRequest = { uuid: uuid ?? null } return this.request<{ message: string }>(endpoint, { method: 'POST', - body: JSON.stringify({ uuid }), + body: JSON.stringify(body), }) } - async getEntityFields( - entityType: string - ): Promise> { - return this.request< - Array<{ name: string; type: string; required: boolean; system: boolean }> - >(`/admin/api/v1/entity-definitions/${encodeURIComponent(entityType)}/fields`) + async getEntityFields(entityType: string): Promise { + return this.request( + `/admin/api/v1/entity-definitions/${encodeURIComponent(entityType)}/fields` + ) } - async listEntityDefinitionVersions(uuid: string): Promise< - Array<{ - version_number: number - created_at: string - created_by?: string | null - created_by_name?: string | null - }> - > { - return this.request< - Array<{ - version_number: number - created_at: string - created_by?: string | null - created_by_name?: string | null - }> - >(`/admin/api/v1/entity-definitions/${uuid}/versions`) + async listEntityDefinitionVersions(uuid: string): Promise { + return this.request( + `/admin/api/v1/entity-definitions/${uuid}/versions` + ) } async getEntityDefinitionVersion( uuid: string, versionNumber: number - ): Promise<{ - version_number: number - created_at: string - created_by?: string | null - data: Record - }> { - return this.request<{ - version_number: number - created_at: string - created_by?: string | null - data: Record - }>(`/admin/api/v1/entity-definitions/${uuid}/versions/${versionNumber}`) + ): Promise { + return this.request( + `/admin/api/v1/entity-definitions/${uuid}/versions/${versionNumber}` + ) } } diff --git a/fe/src/api/clients/meta.ts b/fe/src/api/clients/meta.ts index 4dd8b4fb..029783b1 100644 --- a/fe/src/api/clients/meta.ts +++ b/fe/src/api/clients/meta.ts @@ -1,34 +1,11 @@ +import type { DashboardStats } from '@/types/generated/DashboardStats' import { BaseTypedHttpClient } from './base' -interface EntityTypeCount { - entity_type: string - count: number -} - -interface EntityStats { - total: number - by_type: EntityTypeCount[] -} - -interface WorkflowWithLatestStatus { - uuid: string - name: string - latest_status?: string | null -} - -interface WorkflowStats { - total: number - workflows: WorkflowWithLatestStatus[] -} - -export interface DashboardStats { - entity_definitions_count: number - entities: EntityStats - workflows: WorkflowStats - online_users_count: number -} - -export type { EntityStats, EntityTypeCount, WorkflowStats, WorkflowWithLatestStatus } +export type { DashboardStats } from '@/types/generated/DashboardStats' +export type { EntityStats } from '@/types/generated/EntityStats' +export type { EntityTypeCount } from '@/types/generated/EntityTypeCount' +export type { WorkflowStats } from '@/types/generated/WorkflowStats' +export type { WorkflowWithLatestStatus } from '@/types/generated/WorkflowWithLatestStatus' export class MetaClient extends BaseTypedHttpClient { async getDashboardStats(): Promise { diff --git a/fe/src/api/clients/permissions.test.ts b/fe/src/api/clients/permissions.test.ts index 58ce468c..ad558e53 100644 --- a/fe/src/api/clients/permissions.test.ts +++ b/fe/src/api/clients/permissions.test.ts @@ -82,7 +82,12 @@ describe('RolesClient (permissions)', () => { json: async () => mockResponse, }) - const result = await client.getRoles(1, 10) + const result = await client.getRoles({ + page: 1, + per_page: 10, + limit: null, + offset: null, + }) expect(result.data).toBeDefined() expect(Array.isArray(result.data)).toBe(true) @@ -117,7 +122,12 @@ describe('RolesClient (permissions)', () => { json: async () => mockResponse, }) - const result = await client.getRoles(1, 10) + const result = await client.getRoles({ + page: 1, + per_page: 10, + limit: null, + offset: null, + }) expect(result.data).toHaveLength(0) expect(result.meta?.pagination?.total).toBe(0) @@ -167,6 +177,7 @@ describe('RolesClient (permissions)', () => { const request: CreateRoleRequest = { name: 'Auditor', description: 'Read-only auditing role', + super_admin: null, permissions: [mockPermission], } @@ -204,6 +215,7 @@ describe('RolesClient (permissions)', () => { const request: CreateRoleRequest = { name: '', description: null, + super_admin: null, permissions: [], } @@ -223,6 +235,7 @@ describe('RolesClient (permissions)', () => { const request: UpdateRoleRequest = { name: 'Senior Auditor', description: 'Extended read-only auditing role', + super_admin: null, permissions: [mockPermission], } @@ -259,6 +272,7 @@ describe('RolesClient (permissions)', () => { const request: UpdateRoleRequest = { name: 'Updated', description: null, + super_admin: null, permissions: [], } diff --git a/fe/src/api/clients/query.test.ts b/fe/src/api/clients/query.test.ts new file mode 100644 index 00000000..eae7cfee --- /dev/null +++ b/fe/src/api/clients/query.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from 'vitest' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' +import { buildListQueryString } from './query' + +const allNullPagination: PaginationQuery = { + page: null, + per_page: null, + limit: null, + offset: null, +} + +describe('buildListQueryString', () => { + it('returns empty string when every field is null and no sorting is passed', () => { + expect(buildListQueryString(allNullPagination)).toBe('') + }) + + it('returns empty string when sorting is undefined and pagination is empty', () => { + expect(buildListQueryString(allNullPagination, undefined)).toBe('') + }) + + it('emits page + per_page when both are set', () => { + const pagination: PaginationQuery = { + page: 2, + per_page: 25, + limit: null, + offset: null, + } + expect(buildListQueryString(pagination)).toBe('?page=2&per_page=25') + }) + + it('emits limit + offset when both are set', () => { + const pagination: PaginationQuery = { + page: null, + per_page: null, + limit: 10, + offset: 40, + } + expect(buildListQueryString(pagination)).toBe('?limit=10&offset=40') + }) + + it('preserves zero values (does not treat 0 as absent)', () => { + const pagination: PaginationQuery = { + page: null, + per_page: null, + limit: 20, + offset: 0, + } + expect(buildListQueryString(pagination)).toBe('?limit=20&offset=0') + }) + + it('appends sort_by + sort_order when both are set', () => { + const pagination: PaginationQuery = { + page: 1, + per_page: 10, + limit: null, + offset: null, + } + const sorting: SortingQuery = { sort_by: 'name', sort_order: 'asc' } + expect(buildListQueryString(pagination, sorting)).toBe( + '?page=1&per_page=10&sort_by=name&sort_order=asc' + ) + }) + + it('skips sorting when sort_by is null even if sort_order is set', () => { + const sorting: SortingQuery = { sort_by: null, sort_order: 'desc' } + expect(buildListQueryString(allNullPagination, sorting)).toBe('') + }) + + it('skips sorting when sort_order is null even if sort_by is set', () => { + const sorting: SortingQuery = { sort_by: 'name', sort_order: null } + expect(buildListQueryString(allNullPagination, sorting)).toBe('') + }) + + it('ignores sorting when passed null explicitly', () => { + const pagination: PaginationQuery = { + page: 1, + per_page: 10, + limit: null, + offset: null, + } + expect(buildListQueryString(pagination, null)).toBe('?page=1&per_page=10') + }) + + it('combines page/per_page with sort params correctly', () => { + const pagination: PaginationQuery = { + page: 3, + per_page: 50, + limit: null, + offset: null, + } + const sorting: SortingQuery = { sort_by: 'created_at', sort_order: 'desc' } + expect(buildListQueryString(pagination, sorting)).toBe( + '?page=3&per_page=50&sort_by=created_at&sort_order=desc' + ) + }) +}) diff --git a/fe/src/api/clients/query.ts b/fe/src/api/clients/query.ts new file mode 100644 index 00000000..70b09d25 --- /dev/null +++ b/fe/src/api/clients/query.ts @@ -0,0 +1,23 @@ +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' + +/** + * Serialize typed BE query shapes (PaginationQuery + SortingQuery) into a URL query string. + * Null fields are omitted so the emitted URL only carries the parameters the caller set. + */ +export function buildListQueryString( + pagination: PaginationQuery, + sorting?: SortingQuery | null +): string { + const params = new URLSearchParams() + if (pagination.page != null) params.set('page', String(pagination.page)) + if (pagination.per_page != null) params.set('per_page', String(pagination.per_page)) + if (pagination.limit != null) params.set('limit', String(pagination.limit)) + if (pagination.offset != null) params.set('offset', String(pagination.offset)) + if (sorting?.sort_by != null && sorting.sort_order != null) { + params.set('sort_by', sorting.sort_by) + params.set('sort_order', sorting.sort_order) + } + const qs = params.toString() + return qs ? `?${qs}` : '' +} diff --git a/fe/src/api/clients/roles.test.ts b/fe/src/api/clients/roles.test.ts index 8c68a8b7..70ac56a2 100644 --- a/fe/src/api/clients/roles.test.ts +++ b/fe/src/api/clients/roles.test.ts @@ -82,7 +82,12 @@ describe('RolesClient', () => { json: async () => mockResponse, }) - const result = await client.getRoles(1, 10) + const result = await client.getRoles({ + page: 1, + per_page: 10, + limit: null, + offset: null, + }) expect(result.data).toBeDefined() expect(Array.isArray(result.data)).toBe(true) @@ -117,7 +122,10 @@ describe('RolesClient', () => { json: async () => mockResponse, }) - const result = await client.getRoles(1, 10, 'name', 'asc') + const result = await client.getRoles( + { page: 1, per_page: 10, limit: null, offset: null }, + { sort_by: 'name', sort_order: 'asc' } + ) expect(result.data).toBeDefined() expect(mockFetch).toHaveBeenCalledWith( @@ -150,7 +158,10 @@ describe('RolesClient', () => { json: async () => mockResponse, }) - const result = await client.getRoles(1, 10, 'created_at', 'desc') + const result = await client.getRoles( + { page: 1, per_page: 10, limit: null, offset: null }, + { sort_by: 'created_at', sort_order: 'desc' } + ) expect(result.data).toBeDefined() expect(mockFetch).toHaveBeenCalledWith( @@ -183,7 +194,10 @@ describe('RolesClient', () => { json: async () => mockResponse, }) - const result = await client.getRoles(1, 10, null, null) + const result = await client.getRoles( + { page: 1, per_page: 10, limit: null, offset: null }, + null + ) expect(result.data).toBeDefined() const url = mockFetch.mock.calls[0][0] as string @@ -234,6 +248,7 @@ describe('RolesClient', () => { const request: CreateRoleRequest = { name: 'Editor', description: 'Can edit content', + super_admin: null, permissions: [mockPermission], } @@ -271,6 +286,7 @@ describe('RolesClient', () => { const request: CreateRoleRequest = { name: '', description: null, + super_admin: null, permissions: [], } @@ -290,6 +306,7 @@ describe('RolesClient', () => { const request: UpdateRoleRequest = { name: 'Senior Editor', description: 'Can edit and publish content', + super_admin: null, permissions: [mockPermission], } @@ -325,6 +342,7 @@ describe('RolesClient', () => { const request: UpdateRoleRequest = { name: 'Updated', description: null, + super_admin: null, permissions: [], } diff --git a/fe/src/api/clients/roles.ts b/fe/src/api/clients/roles.ts index c62feb12..e4fa9960 100644 --- a/fe/src/api/clients/roles.ts +++ b/fe/src/api/clients/roles.ts @@ -1,34 +1,23 @@ import type { RoleResponse } from '@/types/generated/RoleResponse' -import type { CreateRoleRequest, UpdateRoleRequest, AssignRolesRequest } from '@/types/schemas' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' +import type { + CreateRoleRequest, + UpdateRoleRequest, + AssignRolesRequest, + ResponseMeta, +} from '@/types/schemas' import { BaseTypedHttpClient } from './base' +import { buildListQueryString } from './query' export class RolesClient extends BaseTypedHttpClient { async getRoles( - page = 1, - itemsPerPage = 20, - sortBy?: string | null, - sortOrder?: 'asc' | 'desc' | null - ): Promise<{ - data: RoleResponse[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - }> { - let url = `/admin/api/v1/roles?page=${page}&per_page=${itemsPerPage}` - if (sortBy && sortOrder) { - url += `&sort_by=${sortBy}&sort_order=${sortOrder}` - } - return this.paginatedRequest(url) + pagination: PaginationQuery, + sorting?: SortingQuery | null + ): Promise<{ data: RoleResponse[]; meta?: ResponseMeta }> { + return this.paginatedRequest( + `/admin/api/v1/roles${buildListQueryString(pagination, sorting)}` + ) } async getRole(uuid: string): Promise { diff --git a/fe/src/api/clients/system-logs.ts b/fe/src/api/clients/system-logs.ts index f86f2169..57d9ed2c 100644 --- a/fe/src/api/clients/system-logs.ts +++ b/fe/src/api/clients/system-logs.ts @@ -1,36 +1,17 @@ import { BaseTypedHttpClient } from './base' import type { SystemLogDto } from '@/types/generated/SystemLogDto' -import type { SystemLogType } from '@/types/generated/SystemLogType' -import type { SystemLogResourceType } from '@/types/generated/SystemLogResourceType' -import type { SystemLogStatus } from '@/types/generated/SystemLogStatus' +import type { SystemLogQuery } from '@/types/generated/SystemLogQuery' +import type { ResponseMeta } from '@/types/schemas' export type SystemLog = SystemLogDto export interface SystemLogListResult { data: SystemLog[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - } + meta?: ResponseMeta } export class SystemLogClient extends BaseTypedHttpClient { - async list(params: { - page?: number - page_size?: number - log_type?: SystemLogType - resource_type?: SystemLogResourceType - status?: SystemLogStatus - resource_uuid?: string - date_from?: string - date_to?: string - }): Promise { + async list(params: SystemLogQuery): Promise { const query = new URLSearchParams() if (params.page) query.set('page', String(params.page)) if (params.page_size) query.set('page_size', String(params.page_size)) diff --git a/fe/src/api/clients/system.test.ts b/fe/src/api/clients/system.test.ts index b7fbd8a4..ddd0a5af 100644 --- a/fe/src/api/clients/system.test.ts +++ b/fe/src/api/clients/system.test.ts @@ -129,6 +129,7 @@ describe('SystemClient', () => { const payload: EntityVersioningSettings = { enabled: true, max_versions: -1, + max_age_days: null, } mockFetch.mockResolvedValueOnce({ @@ -229,6 +230,7 @@ describe('SystemClient', () => { const payload: WorkflowRunLogSettings = { enabled: true, max_runs: -5, + max_age_days: null, } mockFetch.mockResolvedValueOnce({ @@ -279,6 +281,12 @@ describe('SystemClient', () => { it('should handle invalid license status', async () => { const mockStatus: LicenseStatus = { state: 'invalid', + company: null, + license_type: null, + license_id: null, + issued_at: null, + expires_at: null, + version: null, verified_at: '2024-06-15T12:00:00Z', error_message: 'License signature mismatch', } @@ -301,7 +309,14 @@ describe('SystemClient', () => { it('should handle missing license (none state)', async () => { const mockStatus: LicenseStatus = { state: 'none', + company: null, + license_type: null, + license_id: null, + issued_at: null, + expires_at: null, + version: null, verified_at: '2024-06-15T12:00:00Z', + error_message: null, } mockFetch.mockResolvedValueOnce({ @@ -316,7 +331,7 @@ describe('SystemClient', () => { const result = await client.getLicenseStatus() expect(result.state).toBe('none') - expect(result.company).toBeUndefined() + expect(result.company).toBeNull() }) }) diff --git a/fe/src/api/clients/system.ts b/fe/src/api/clients/system.ts index ab0c6721..2d568484 100644 --- a/fe/src/api/clients/system.ts +++ b/fe/src/api/clients/system.ts @@ -1,54 +1,30 @@ +import type { EntityVersioningSettingsDto } from '@/types/generated/EntityVersioningSettingsDto' +import type { WorkflowRunLogSettingsDto } from '@/types/generated/WorkflowRunLogSettingsDto' +import type { UpdateSettingsBody } from '@/types/generated/UpdateSettingsBody' +import type { UpdateWorkflowRunLogSettingsBody } from '@/types/generated/UpdateWorkflowRunLogSettingsBody' +import type { LicenseStatusDto } from '@/types/generated/LicenseStatusDto' +import type { SystemVersionsDto } from '@/types/generated/SystemVersionsDto' import { BaseTypedHttpClient } from './base' -export interface EntityVersioningSettings { - enabled: boolean - max_versions?: number | null - max_age_days?: number | null -} - -export interface WorkflowRunLogSettings { - enabled: boolean - max_runs?: number | null - max_age_days?: number | null -} - -export type LicenseState = 'none' | 'invalid' | 'error' | 'valid' - -export interface LicenseStatus { - state: LicenseState - company?: string | null - license_type?: string | null - license_id?: string | null - issued_at?: string | null - expires_at?: string | null - version?: string | null - verified_at: string - error_message?: string | null -} - -export interface ComponentVersion { - name: string - version: string - last_seen_at: string -} - -export interface SystemVersions { - core: string - worker?: ComponentVersion | null - maintenance?: ComponentVersion | null -} +// FE-facing aliases over BE-generated shapes; callers keep using short names without redeclaring. +export type EntityVersioningSettings = EntityVersioningSettingsDto +export type WorkflowRunLogSettings = WorkflowRunLogSettingsDto +export type LicenseStatus = LicenseStatusDto +export type SystemVersions = SystemVersionsDto +export type { LicenseStateDto as LicenseState } from '@/types/generated/LicenseStateDto' +export type { ComponentVersionDto as ComponentVersion } from '@/types/generated/ComponentVersionDto' export class SystemClient extends BaseTypedHttpClient { - async getEntityVersioningSettings(): Promise { - return this.request( + async getEntityVersioningSettings(): Promise { + return this.request( '/admin/api/v1/system/settings/entity-versioning' ) } async updateEntityVersioningSettings( - payload: EntityVersioningSettings - ): Promise { - return this.request( + payload: UpdateSettingsBody + ): Promise { + return this.request( '/admin/api/v1/system/settings/entity-versioning', { method: 'PUT', @@ -57,16 +33,16 @@ export class SystemClient extends BaseTypedHttpClient { ) } - async getWorkflowRunLogSettings(): Promise { - return this.request( + async getWorkflowRunLogSettings(): Promise { + return this.request( '/admin/api/v1/system/settings/workflow-run-logs' ) } async updateWorkflowRunLogSettings( - payload: WorkflowRunLogSettings - ): Promise { - return this.request( + payload: UpdateWorkflowRunLogSettingsBody + ): Promise { + return this.request( '/admin/api/v1/system/settings/workflow-run-logs', { method: 'PUT', @@ -75,11 +51,11 @@ export class SystemClient extends BaseTypedHttpClient { ) } - async getLicenseStatus(): Promise { - return this.request('/admin/api/v1/system/license') + async getLicenseStatus(): Promise { + return this.request('/admin/api/v1/system/license') } - async getSystemVersions(): Promise { - return this.request('/admin/api/v1/system/versions') + async getSystemVersions(): Promise { + return this.request('/admin/api/v1/system/versions') } } diff --git a/fe/src/api/clients/users.test.ts b/fe/src/api/clients/users.test.ts index 3e906f62..f44d89a6 100644 --- a/fe/src/api/clients/users.test.ts +++ b/fe/src/api/clients/users.test.ts @@ -78,7 +78,12 @@ describe('UsersClient', () => { json: async () => mockResponse, }) - const result = await client.getUsers(1, 10) + const result = await client.getUsers({ + page: 1, + per_page: 10, + limit: null, + offset: null, + }) expect(result.data).toBeDefined() expect(Array.isArray(result.data)).toBe(true) @@ -113,7 +118,10 @@ describe('UsersClient', () => { json: async () => mockResponse, }) - const result = await client.getUsers(1, 10, 'username', 'asc') + const result = await client.getUsers( + { page: 1, per_page: 10, limit: null, offset: null }, + { sort_by: 'username', sort_order: 'asc' } + ) expect(result.data).toBeDefined() expect(mockFetch).toHaveBeenCalledWith( @@ -146,7 +154,10 @@ describe('UsersClient', () => { json: async () => mockResponse, }) - const result = await client.getUsers(1, 10, 'created_at', 'desc') + const result = await client.getUsers( + { page: 1, per_page: 10, limit: null, offset: null }, + { sort_by: 'created_at', sort_order: 'desc' } + ) expect(result.data).toBeDefined() expect(mockFetch).toHaveBeenCalledWith( @@ -179,7 +190,10 @@ describe('UsersClient', () => { json: async () => mockResponse, }) - const result = await client.getUsers(1, 10, null, null) + const result = await client.getUsers( + { page: 1, per_page: 10, limit: null, offset: null }, + null + ) expect(result.data).toBeDefined() const url = mockFetch.mock.calls[0][0] as string @@ -233,6 +247,9 @@ describe('UsersClient', () => { password: 'securepassword123', first_name: 'New', last_name: 'User', + role_uuids: null, + is_active: null, + super_admin: null, } const mockResponse = { @@ -272,6 +289,9 @@ describe('UsersClient', () => { password: '123', first_name: 'New', last_name: 'User', + role_uuids: null, + is_active: null, + super_admin: null, } mockFetch.mockResolvedValueOnce({ @@ -289,7 +309,12 @@ describe('UsersClient', () => { it('should update a user successfully', async () => { const request: UpdateUserRequest = { email: 'updated@example.com', + password: null, first_name: 'Updated', + last_name: null, + role_uuids: null, + is_active: null, + super_admin: null, } const mockResponse = { @@ -323,6 +348,12 @@ describe('UsersClient', () => { it('should handle 404 error when updating non-existent user', async () => { const request: UpdateUserRequest = { email: 'updated@example.com', + password: null, + first_name: null, + last_name: null, + role_uuids: null, + is_active: null, + super_admin: null, } mockFetch.mockResolvedValueOnce({ diff --git a/fe/src/api/clients/users.ts b/fe/src/api/clients/users.ts index 86006ed4..11578171 100644 --- a/fe/src/api/clients/users.ts +++ b/fe/src/api/clients/users.ts @@ -1,34 +1,18 @@ import type { UserResponse } from '@/types/generated/UserResponse' -import type { CreateUserRequest, UpdateUserRequest } from '@/types/schemas' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' +import type { CreateUserRequest, UpdateUserRequest, ResponseMeta } from '@/types/schemas' import { BaseTypedHttpClient } from './base' +import { buildListQueryString } from './query' export class UsersClient extends BaseTypedHttpClient { async getUsers( - page = 1, - itemsPerPage = 20, - sortBy?: string | null, - sortOrder?: 'asc' | 'desc' | null - ): Promise<{ - data: UserResponse[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - request_id?: string - timestamp?: string - custom?: unknown - } - }> { - let url = `/admin/api/v1/users?page=${page}&per_page=${itemsPerPage}` - if (sortBy && sortOrder) { - url += `&sort_by=${sortBy}&sort_order=${sortOrder}` - } - return this.paginatedRequest(url) + pagination: PaginationQuery, + sorting?: SortingQuery | null + ): Promise<{ data: UserResponse[]; meta?: ResponseMeta }> { + return this.paginatedRequest( + `/admin/api/v1/users${buildListQueryString(pagination, sorting)}` + ) } async getUser(uuid: string): Promise { diff --git a/fe/src/api/clients/workflows.test.ts b/fe/src/api/clients/workflows.test.ts index 317d0ffe..608592ab 100644 --- a/fe/src/api/clients/workflows.test.ts +++ b/fe/src/api/clients/workflows.test.ts @@ -173,7 +173,12 @@ describe('WorkflowsClient', () => { successResponse([mockWorkflowSummary], { pagination: mockPagination }), }) - const result = await client.getWorkflows() + const result = await client.getWorkflows({ + page: 1, + per_page: 20, + limit: null, + offset: null, + }) expect(result.data).toHaveLength(1) expect(result.meta?.pagination?.total).toBe(50) @@ -190,7 +195,7 @@ describe('WorkflowsClient', () => { successResponse([mockWorkflowSummary], { pagination: mockPagination }), }) - await client.getWorkflows(3, 50) + await client.getWorkflows({ page: 3, per_page: 50, limit: null, offset: null }) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('page=3&per_page=50'), @@ -205,7 +210,10 @@ describe('WorkflowsClient', () => { successResponse([mockWorkflowSummary], { pagination: mockPagination }), }) - await client.getWorkflows(1, 20, 'name', 'asc') + await client.getWorkflows( + { page: 1, per_page: 20, limit: null, offset: null }, + { sort_by: 'name', sort_order: 'asc' } + ) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('sort_by=name&sort_order=asc'), @@ -219,7 +227,7 @@ describe('WorkflowsClient', () => { json: async () => successResponse([mockWorkflowSummary]), }) - await client.getWorkflows(1, 20) + await client.getWorkflows({ page: 1, per_page: 20, limit: null, offset: null }) const calledUrl: string = mockFetch.mock.calls[0][0] as string expect(calledUrl).not.toContain('sort_by') @@ -234,7 +242,12 @@ describe('WorkflowsClient', () => { json: async () => successResponse([summaryProviderKind]), }) - const result = await client.getWorkflows() + const result = await client.getWorkflows({ + page: 1, + per_page: 20, + limit: null, + offset: null, + }) expect(result.data[0].kind).toBe('provider') }) @@ -299,10 +312,11 @@ describe('WorkflowsClient', () => { const newWorkflow = { name: 'New Workflow', description: 'A brand-new workflow', - kind: 'consumer' as const, + kind: 'consumer', enabled: true, schedule_cron: null, config: mockWorkflowConfig, + versioning_disabled: false, } it('should create a workflow and return uuid', async () => { @@ -355,9 +369,12 @@ describe('WorkflowsClient', () => { describe('updateWorkflow', () => { const updatedWorkflow = { name: 'Updated Workflow', - kind: 'consumer' as const, + description: null, + kind: 'consumer', enabled: false, + schedule_cron: null, config: mockWorkflowConfig, + versioning_disabled: false, } it('should update a workflow and return message', async () => { @@ -510,7 +527,12 @@ describe('WorkflowsClient', () => { successResponse([mockWorkflowRun], { pagination: mockPagination }), }) - const result = await client.getWorkflowRuns('wf-uuid-1') + const result = await client.getWorkflowRuns('wf-uuid-1', { + page: 1, + per_page: 20, + limit: null, + offset: null, + }) expect(result.data).toHaveLength(1) expect(result.data[0].uuid).toBe('run-uuid-1') @@ -527,7 +549,12 @@ describe('WorkflowsClient', () => { json: async () => successResponse([mockWorkflowRun]), }) - await client.getWorkflowRuns('wf-uuid-1', 2, 10) + await client.getWorkflowRuns('wf-uuid-1', { + page: 2, + per_page: 10, + limit: null, + offset: null, + }) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('page=2&per_page=10'), @@ -546,7 +573,12 @@ describe('WorkflowsClient', () => { successResponse([mockWorkflowRunLog], { pagination: mockPagination }), }) - const result = await client.getWorkflowRunLogs('run-uuid-1') + const result = await client.getWorkflowRunLogs('run-uuid-1', { + page: 1, + per_page: 50, + limit: null, + offset: null, + }) expect(result.data).toHaveLength(1) expect(result.data[0].uuid).toBe('log-uuid-1') @@ -563,7 +595,12 @@ describe('WorkflowsClient', () => { json: async () => successResponse([mockWorkflowRunLog]), }) - await client.getWorkflowRunLogs('run-uuid-1') + await client.getWorkflowRunLogs('run-uuid-1', { + page: 1, + per_page: 50, + limit: null, + offset: null, + }) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('per_page=50'), @@ -577,7 +614,12 @@ describe('WorkflowsClient', () => { json: async () => successResponse([]), }) - await client.getWorkflowRunLogs('run-uuid-1', 3, 100) + await client.getWorkflowRunLogs('run-uuid-1', { + page: 3, + per_page: 100, + limit: null, + offset: null, + }) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('page=3&per_page=100'), @@ -596,7 +638,12 @@ describe('WorkflowsClient', () => { successResponse([mockWorkflowRun], { pagination: mockPagination }), }) - const result = await client.getAllWorkflowRuns() + const result = await client.getAllWorkflowRuns({ + page: 1, + per_page: 20, + limit: null, + offset: null, + }) expect(result.data).toHaveLength(1) expect(result.meta?.pagination?.has_next).toBe(true) @@ -612,7 +659,12 @@ describe('WorkflowsClient', () => { json: async () => successResponse([]), }) - await client.getAllWorkflowRuns(4, 25) + await client.getAllWorkflowRuns({ + page: 4, + per_page: 25, + limit: null, + offset: null, + }) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('page=4&per_page=25'), diff --git a/fe/src/api/clients/workflows.ts b/fe/src/api/clients/workflows.ts index a55ef078..8b160637 100644 --- a/fe/src/api/clients/workflows.ts +++ b/fe/src/api/clients/workflows.ts @@ -1,8 +1,18 @@ import type { WorkflowDetail } from '@/types/generated/WorkflowDetail' import type { WorkflowSummary } from '@/types/generated/WorkflowSummary' import type { WorkflowRunLogDto } from '@/types/generated/WorkflowRunLogDto' -import type { DslOptionsResponse, WorkflowRun, WorkflowConfig } from '@/types/schemas' +import type { CreateWorkflowResponse } from '@/types/generated/CreateWorkflowResponse' +import type { CreateWorkflowRequest } from '@/types/generated/CreateWorkflowRequest' +import type { UpdateWorkflowRequest } from '@/types/generated/UpdateWorkflowRequest' +import type { WorkflowVersionMeta } from '@/types/generated/WorkflowVersionMeta' +import type { WorkflowVersionPayload } from '@/types/generated/WorkflowVersionPayload' +import type { WorkflowRunUploadResponse } from '@/types/generated/WorkflowRunUploadResponse' +import type { DslValidateResponse } from '@/types/generated/DslValidateResponse' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' +import type { DslOptionsResponse, WorkflowRun, ResponseMeta } from '@/types/schemas' import { BaseTypedHttpClient } from './base' +import { buildListQueryString } from './query' import { useAuthStore } from '@/stores/auth' import { buildApiUrl } from '@/env-check' @@ -12,61 +22,26 @@ export class WorkflowsClient extends BaseTypedHttpClient { } async getWorkflows( - page = 1, - itemsPerPage = 20, - sortBy?: string | null, - sortOrder?: 'asc' | 'desc' | null - ): Promise<{ - data: WorkflowSummary[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - } - }> { - let url = `/admin/api/v1/workflows?page=${page}&per_page=${itemsPerPage}` - if (sortBy && sortOrder) { - url += `&sort_by=${sortBy}&sort_order=${sortOrder}` - } - return this.paginatedRequest(url) + pagination: PaginationQuery, + sorting?: SortingQuery | null + ): Promise<{ data: WorkflowSummary[]; meta?: ResponseMeta }> { + return this.paginatedRequest( + `/admin/api/v1/workflows${buildListQueryString(pagination, sorting)}` + ) } async getWorkflow(uuid: string): Promise { return this.request(`/admin/api/v1/workflows/${uuid}`) } - async createWorkflow(data: { - name: string - description?: string | null - kind: 'consumer' | 'provider' - enabled: boolean - schedule_cron?: string | null - config: WorkflowConfig - versioning_disabled?: boolean - }): Promise<{ uuid: string }> { - return this.request<{ uuid: string }>('/admin/api/v1/workflows', { + async createWorkflow(data: CreateWorkflowRequest): Promise { + return this.request('/admin/api/v1/workflows', { method: 'POST', body: JSON.stringify(data), }) } - async updateWorkflow( - uuid: string, - data: { - name: string - description?: string | null - kind: 'consumer' | 'provider' - enabled: boolean - schedule_cron?: string | null - config: WorkflowConfig - versioning_disabled?: boolean - } - ): Promise<{ message: string }> { + async updateWorkflow(uuid: string, data: UpdateWorkflowRequest): Promise<{ message: string }> { return this.request<{ message: string }>(`/admin/api/v1/workflows/${uuid}`, { method: 'PUT', body: JSON.stringify(data), @@ -93,73 +68,31 @@ export class WorkflowsClient extends BaseTypedHttpClient { async getWorkflowRuns( workflowUuid: string, - page = 1, - perPage = 20 - ): Promise<{ - data: WorkflowRun[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - } - }> { + pagination: PaginationQuery + ): Promise<{ data: WorkflowRun[]; meta?: ResponseMeta }> { return this.paginatedRequest( - `/admin/api/v1/workflows/${workflowUuid}/runs?page=${page}&per_page=${perPage}` + `/admin/api/v1/workflows/${workflowUuid}/runs${buildListQueryString(pagination)}` ) } async getWorkflowRunLogs( runUuid: string, - page = 1, - perPage = 50 - ): Promise<{ - data: WorkflowRunLogDto[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - } - }> { + pagination: PaginationQuery + ): Promise<{ data: WorkflowRunLogDto[]; meta?: ResponseMeta }> { return this.paginatedRequest( - `/admin/api/v1/workflows/runs/${runUuid}/logs?page=${page}&per_page=${perPage}` + `/admin/api/v1/workflows/runs/${runUuid}/logs${buildListQueryString(pagination)}` ) } async getAllWorkflowRuns( - page = 1, - perPage = 20 - ): Promise<{ - data: WorkflowRun[] - meta?: { - pagination?: { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } - } - }> { + pagination: PaginationQuery + ): Promise<{ data: WorkflowRun[]; meta?: ResponseMeta }> { return this.paginatedRequest( - `/admin/api/v1/workflows/runs?page=${page}&per_page=${perPage}` + `/admin/api/v1/workflows/runs${buildListQueryString(pagination)}` ) } - async uploadRunFile( - workflowUuid: string, - file: File - ): Promise<{ run_uuid: string; staged_items: number }> { + async uploadRunFile(workflowUuid: string, file: File): Promise { const form = new FormData() form.append('file', file) // Bypass JSON content-type; handle raw fetch here due to multipart @@ -172,7 +105,6 @@ export class WorkflowsClient extends BaseTypedHttpClient { body: form, }) if (!res.ok) { - // Try to extract standardized error try { const err = await res.json() if (err?.message) { @@ -186,7 +118,7 @@ export class WorkflowsClient extends BaseTypedHttpClient { const json = (await res.json()) as { status: string message: string - data?: { run_uuid: string; staged_items: number } | null + data?: WorkflowRunUploadResponse | null } if (!json.data) { @@ -208,45 +140,20 @@ export class WorkflowsClient extends BaseTypedHttpClient { return this.request('/admin/api/v1/dsl/transform/options') } - async validateDsl(steps: import('@/types/schemas').DslStep[]): Promise<{ valid: boolean }> { - return this.request<{ valid: boolean }>('/admin/api/v1/dsl/validate', { + async validateDsl(steps: import('@/types/schemas').DslStep[]): Promise { + return this.request('/admin/api/v1/dsl/validate', { method: 'POST', body: JSON.stringify({ steps }), }) } - async listWorkflowVersions(uuid: string): Promise< - Array<{ - version_number: number - created_at: string - created_by?: string | null - created_by_name?: string | null - }> - > { - return this.request< - Array<{ - version_number: number - created_at: string - created_by?: string | null - created_by_name?: string | null - }> - >(`/admin/api/v1/workflows/${uuid}/versions`) - } - - async getWorkflowVersion( - uuid: string, - versionNumber: number - ): Promise<{ - version_number: number - created_at: string - created_by?: string | null - data: Record - }> { - return this.request<{ - version_number: number - created_at: string - created_by?: string | null - data: Record - }>(`/admin/api/v1/workflows/${uuid}/versions/${versionNumber}`) + async listWorkflowVersions(uuid: string): Promise { + return this.request(`/admin/api/v1/workflows/${uuid}/versions`) + } + + async getWorkflowVersion(uuid: string, versionNumber: number): Promise { + return this.request( + `/admin/api/v1/workflows/${uuid}/versions/${versionNumber}` + ) } } diff --git a/fe/src/api/http-client.ts b/fe/src/api/http-client.ts index 371925b7..4658c1a3 100644 --- a/fe/src/api/http-client.ts +++ b/fe/src/api/http-client.ts @@ -1,325 +1,12 @@ -import { z } from 'zod' -import { env, buildApiUrl } from '@/env-check' -import { useAuthStore } from '@/stores/auth' -import { getRefreshToken } from '@/utils/cookies' -import { ValidationErrorResponseSchema } from '@/types/schemas' -import type { Meta } from '@/types/schemas' -import { HttpError, extractNamespaceFromEndpoint, extractActionFromMethod } from './errors' +import type { ValidationViolation } from '@/types/schemas' -// Custom error class for validation errors +// Custom error class for validation errors thrown by api/clients/base.ts. export class ValidationError extends Error { - violations: Array<{ field: string; message: string; code?: string }> + violations: ValidationViolation[] - constructor( - message: string, - violations: Array<{ field: string; message: string; code?: string }> - ) { + constructor(message: string, violations: ValidationViolation[]) { super(message) this.name = 'ValidationError' this.violations = violations } } - -// Define ApiResponse type -export type ApiResponse = { - status: 'Success' | 'Error' - message: string - data?: T - meta?: Meta -} - -export class HttpClient { - protected enableLogging = env.enableApiLogging - protected devMode = env.devMode - private isRefreshing = false // Re-entrancy guard for token refresh - - async request( - endpoint: string, - schema: z.ZodType>, - options: RequestInit = {} - ): Promise { - // Get auth token from auth store - const authStore = useAuthStore() - let authToken = authStore.token - - // If no token in store but refresh token exists, try to refresh - if (!authToken) { - const refreshToken = getRefreshToken() - if (refreshToken && !endpoint.includes('/auth/refresh') && !this.isRefreshing) { - if (this.enableLogging) { - console.log( - '[API] No access token but refresh token exists, attempting refresh' - ) - } - try { - this.isRefreshing = true - // Trigger refresh through auth store - await authStore.refreshTokens() - authToken = authStore.token - } catch (refreshError) { - if (this.enableLogging) { - console.error('[API] Automatic token refresh failed:', refreshError) - } - // Don't logout here, let the 401 handler deal with it - } finally { - this.isRefreshing = false - } - } - } - - const config: RequestInit = { - ...options, - headers: { - 'Content-Type': 'application/json', - ...(authToken && { - Authorization: `Bearer ${authToken}`, - }), - ...options.headers, - }, - } - - try { - const fullUrl = buildApiUrl(endpoint) - if (this.enableLogging) { - console.log(`[API] ${config.method ?? 'GET'} ${fullUrl}`) - } - - const response = await fetch(fullUrl, config) - - if (!response.ok) { - if (response.status === 401) { - // Handle unauthorized - try refresh first, then clear auth - const refreshToken = getRefreshToken() - if (refreshToken && !endpoint.includes('/auth/refresh')) { - if (this.isRefreshing) { - // Another request is already refreshing — wait for it via - // the auth store's shared promise instead of skipping to logout - try { - await authStore.refreshTokens() - const newToken = authStore.token - if (newToken) { - const retryConfig = { - ...config, - headers: { - ...config.headers, - Authorization: `Bearer ${newToken}`, - }, - } - const retryResponse = await fetch( - buildApiUrl(endpoint), - retryConfig - ) - if (retryResponse.ok) { - const retryData = await retryResponse.json() - return this.validateResponse(retryData, schema) - } - } - } catch (refreshError) { - if (this.enableLogging) { - console.error( - '[API] Token refresh failed (concurrent):', - refreshError - ) - } - } - } else { - // Primary refresh path — we own the refresh - try { - if (this.enableLogging) { - console.log('[API] 401 received, attempting token refresh') - } - - this.isRefreshing = true - await authStore.refreshTokens() - const newToken = authStore.token - - if (newToken) { - // Retry the original request with new token - const retryConfig = { - ...config, - headers: { - ...config.headers, - Authorization: `Bearer ${newToken}`, - }, - } - const retryResponse = await fetch( - buildApiUrl(endpoint), - retryConfig - ) - - if (retryResponse.ok) { - const retryData = await retryResponse.json() - return this.validateResponse(retryData, schema) - } - } - } catch (refreshError) { - if (this.enableLogging) { - console.error('[API] Token refresh failed:', refreshError) - } - } finally { - this.isRefreshing = false - } - } - } - - // Clear auth and redirect to login - await authStore.logout() - const namespace = extractNamespaceFromEndpoint(endpoint) - const action = extractActionFromMethod(options.method) - throw new HttpError( - 401, - namespace, - action, - 'Authentication required', - 'Authentication required' - ) - } - - // Try to extract error message from response - try { - const errorData = await response.json() - const statusCode = response.status - - // Determine if this is an expected/handled error (not a true error) - const isExpectedError = [400, 409, 422].includes(statusCode) - - // Only log as error for unexpected status codes; expected ones are handled gracefully - if (this.enableLogging && !isExpectedError) { - console.error('[API] HTTP Error Response:', { - status: statusCode, - statusText: response.statusText, - errorData, - endpoint, - }) - } else if (this.enableLogging && this.devMode) { - // In dev mode, log expected errors as info for debugging - console.log('[API] Handled HTTP Response:', { - status: statusCode, - statusText: response.statusText, - errorData, - endpoint, - }) - } - - const namespace = extractNamespaceFromEndpoint(endpoint) - const action = extractActionFromMethod(options.method) - - // Handle validation errors (422) and bad request errors (400) with structured violations - if ((statusCode === 422 || statusCode === 400) && errorData.violations) { - try { - const validationError = ValidationErrorResponseSchema.parse(errorData) - throw new ValidationError( - validationError.message, - validationError.violations - ) - } catch (parseError) { - // Re-throw ValidationError as-is - if (parseError instanceof ValidationError) { - throw parseError - } - // If parsing fails, log and treat as regular error - if (this.enableLogging) { - console.error('[API] Failed to parse validation error:', parseError) - } - const message = errorData.message ?? response.statusText - throw new HttpError(statusCode, namespace, action, message, message) - } - } - - // Handle backend API response format - if (errorData.status === 'Error' && errorData.message) { - const message = errorData.message - throw new HttpError(statusCode, namespace, action, message, message) - } - - // Handle other error formats - const message = errorData.message ?? errorData.error ?? response.statusText - throw new HttpError(statusCode, namespace, action, message, message) - } catch (parseError) { - // Re-throw validation errors as-is silently - if (parseError instanceof ValidationError) { - throw parseError - } - // Re-throw HttpError as-is - if (parseError instanceof HttpError) { - throw parseError - } - // Only log non-validation errors - if (this.enableLogging) { - console.error('[API] Failed to parse error response:', parseError) - } - const namespace = extractNamespaceFromEndpoint(endpoint) - const action = extractActionFromMethod(options.method) - throw new HttpError( - response.status, - namespace, - action, - response.statusText, - response.statusText - ) - } - } - - const rawData = await response.json() - return this.validateResponse(rawData, schema) - } catch (error) { - // Don't log expected errors (ValidationError, HttpError) to console as they're handled behavior - if (!(error instanceof ValidationError) && !(error instanceof HttpError)) { - if (this.enableLogging) { - console.error('[API] Error:', { - error: error instanceof Error ? error.message : error, - endpoint, - stack: error instanceof Error ? error.stack : undefined, - }) - } - } - throw error - } - } - - protected validateResponse(rawData: unknown, schema: z.ZodType>): T { - if (this.enableLogging && this.devMode) { - console.log('[API] Response:', rawData) - } - - // Runtime validation with Zod - let validatedResponse - try { - validatedResponse = schema.parse(rawData) - } catch (validationError) { - if (validationError instanceof z.ZodError) { - if (this.enableLogging) { - console.error('[API] Response validation failed:', { - rawData, - validationIssues: validationError.issues, - }) - } - // Create a more user-friendly error message - const firstIssue = validationError.issues[0] - const fieldPath = firstIssue.path.join('.') || 'unknown field' - const message = firstIssue.message || 'Invalid format' - throw new Error(`Response validation failed: ${message} (${fieldPath})`) - } - throw validationError - } - - if (validatedResponse.status === 'Error') { - throw new Error(validatedResponse.message) - } - - // For responses with null data (like logout), return the message - if (validatedResponse.data === null) { - return { message: validatedResponse.message } as T - } - - if (!validatedResponse.data) { - throw new Error('No data in successful response') - } - - return validatedResponse.data - } - - protected getDefaultPageSize(): number { - return env.defaultPageSize - } -} diff --git a/fe/src/api/typed-client.ts b/fe/src/api/typed-client.ts index 7ad7b9e3..bd513ec9 100644 --- a/fe/src/api/typed-client.ts +++ b/fe/src/api/typed-client.ts @@ -1,14 +1,9 @@ -// Re-export for backward compatibility -// New code should import from './clients/index' or specific client files import { TypedHttpClient } from './clients/index' -export { TypedHttpClient } export { ValidationError } from './http-client' -export { HttpClient as TypedHttpClientBase } from './http-client' -export type { ApiResponse } from './http-client' -// Re-export all types for convenience +// Re-export generated + Zod-derived types so components can `import type { ... } from '@/api/typed-client'`. export type * from '@/types/schemas' -// Create and export singleton instance +// Singleton — the FE's single entry point for BE calls. export const typedHttpClient = new TypedHttpClient() diff --git a/fe/src/components/api-keys/ApiKeyCreateDialog.vue b/fe/src/components/api-keys/ApiKeyCreateDialog.vue index 254adbb8..42b590f5 100644 --- a/fe/src/components/api-keys/ApiKeyCreateDialog.vue +++ b/fe/src/components/api-keys/ApiKeyCreateDialog.vue @@ -84,8 +84,8 @@ const createFormValid = ref(false) const createForm = ref({ name: '', - description: '', - expires_in_days: undefined, + description: null, + expires_in_days: null, }) // Refs for form validation @@ -106,8 +106,8 @@ const resetForm = () => { createForm.value = { name: '', - description: '', - expires_in_days: undefined, + description: null, + expires_in_days: null, } createFormValid.value = false } @@ -124,8 +124,8 @@ // Create a clean object without circular references const requestData: CreateApiKeyRequest = { name: createForm.value.name, - description: createForm.value.description ?? undefined, - expires_in_days: createForm.value.expires_in_days ?? undefined, + description: createForm.value.description ?? null, + expires_in_days: createForm.value.expires_in_days ?? null, } emit('create', requestData) diff --git a/fe/src/components/common/DataTable.vue b/fe/src/components/common/DataTable.vue deleted file mode 100644 index 87f2e0af..00000000 --- a/fe/src/components/common/DataTable.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - diff --git a/fe/src/components/common/LicenseBanner.test.ts b/fe/src/components/common/LicenseBanner.test.ts index f01b43ad..7162004f 100644 --- a/fe/src/components/common/LicenseBanner.test.ts +++ b/fe/src/components/common/LicenseBanner.test.ts @@ -32,6 +32,7 @@ describe('LicenseBanner', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: null, @@ -53,6 +54,7 @@ describe('LicenseBanner', () => { license_type: null, license_id: null, issued_at: null, + expires_at: null, version: null, verified_at: '2024-01-02T00:00:00Z', error_message: null, @@ -76,6 +78,7 @@ describe('LicenseBanner', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: 'Invalid license key', @@ -99,6 +102,7 @@ describe('LicenseBanner', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: 'Network error', @@ -122,6 +126,7 @@ describe('LicenseBanner', () => { license_type: null, license_id: null, issued_at: null, + expires_at: null, version: null, verified_at: '2024-01-02T00:00:00Z', error_message: null, @@ -144,6 +149,7 @@ describe('LicenseBanner', () => { license_type: null, license_id: null, issued_at: null, + expires_at: null, version: null, verified_at: '2024-01-02T00:00:00Z', error_message: null, diff --git a/fe/src/components/entities/EntityCreateDialog.test.ts b/fe/src/components/entities/EntityCreateDialog.test.ts index 8d0a470f..b8e1ae6c 100644 --- a/fe/src/components/entities/EntityCreateDialog.test.ts +++ b/fe/src/components/entities/EntityCreateDialog.test.ts @@ -398,6 +398,7 @@ describe('EntityCreateDialog', () => { path: '/entity1', entity_uuid: 'uuid-1', entity_type: 'test_type', + has_children: null, published: true, }, ], diff --git a/fe/src/components/permissions/RolePermissionsEditor.vue b/fe/src/components/permissions/RolePermissionsEditor.vue deleted file mode 100644 index c6b96522..00000000 --- a/fe/src/components/permissions/RolePermissionsEditor.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - diff --git a/fe/src/components/system/SystemLogsViewer.vue b/fe/src/components/system/SystemLogsViewer.vue index dbd060d3..56a8b1f3 100644 --- a/fe/src/components/system/SystemLogsViewer.vue +++ b/fe/src/components/system/SystemLogsViewer.vue @@ -375,12 +375,12 @@ const result = await typedHttpClient.listSystemLogs({ page: page.value, page_size: itemsPerPage.value, - log_type: filterLogType.value ?? undefined, - resource_type: filterResourceType.value ?? undefined, - status: filterStatus.value ?? undefined, - resource_uuid: filterResourceUuid.value ?? undefined, - date_from: toIso8601(filterDateFrom.value), - date_to: toIso8601(filterDateTo.value), + log_type: filterLogType.value ?? null, + resource_type: filterResourceType.value ?? null, + status: filterStatus.value ?? null, + resource_uuid: filterResourceUuid.value ?? null, + date_from: toIso8601(filterDateFrom.value) ?? null, + date_to: toIso8601(filterDateTo.value) ?? null, }) logs.value = result.data totalItems.value = result.meta?.pagination?.total ?? 0 diff --git a/fe/src/components/users/UserDialog.vue b/fe/src/components/users/UserDialog.vue index 6f55debc..e973d550 100644 --- a/fe/src/components/users/UserDialog.vue +++ b/fe/src/components/users/UserDialog.vue @@ -225,16 +225,13 @@ if (props.editingUser) { const updateData: UpdateUserRequest = { email: formData.value.email, + password: formData.value.password || null, first_name: formData.value.first_name, last_name: formData.value.last_name, - role_uuids: formData.value.role_uuids ?? undefined, + role_uuids: formData.value.role_uuids ?? null, is_active: formData.value.is_active, super_admin: formData.value.super_admin, } - // Only include password if provided - if (formData.value.password) { - updateData.password = formData.value.password - } emit('save', updateData) } else { const createData: CreateUserRequest = { @@ -243,7 +240,7 @@ password: formData.value.password, first_name: formData.value.first_name, last_name: formData.value.last_name, - role_uuids: formData.value.role_uuids ?? undefined, + role_uuids: formData.value.role_uuids ?? null, is_active: formData.value.is_active, super_admin: formData.value.super_admin, } diff --git a/fe/src/components/workflows/DslClient.test.ts b/fe/src/components/workflows/DslClient.test.ts index f5bf0803..5f5fe083 100644 --- a/fe/src/components/workflows/DslClient.test.ts +++ b/fe/src/components/workflows/DslClient.test.ts @@ -92,7 +92,9 @@ describe('DSL client', () => { // Backend responds with 422 Symfony-style const body = { message: 'Invalid DSL', - violations: [{ field: 'dsl', message: 'mapping must contain at least one field' }], + violations: [ + { field: 'dsl', message: 'mapping must contain at least one field', code: null }, + ], } vi.spyOn(global, 'fetch').mockResolvedValueOnce( new Response(JSON.stringify(body), { diff --git a/fe/src/components/workflows/dsl/DslToEditor.vue b/fe/src/components/workflows/dsl/DslToEditor.vue index 2ecc1737..0cce9216 100644 --- a/fe/src/components/workflows/dsl/DslToEditor.vue +++ b/fe/src/components/workflows/dsl/DslToEditor.vue @@ -565,7 +565,7 @@ async function loadEmailTemplates() { try { - const templates = await typedHttpClient.listEmailTemplates('workflow') + const templates = await typedHttpClient.listEmailTemplates({ type: 'workflow' }) emailTemplateItems.value = templates.map(tmpl => ({ title: tmpl.name, value: tmpl.uuid, diff --git a/fe/src/components/workflows/dsl/DslTransformEditor.vue b/fe/src/components/workflows/dsl/DslTransformEditor.vue index 0e485acb..ce7b2a7e 100644 --- a/fe/src/components/workflows/dsl/DslTransformEditor.vue +++ b/fe/src/components/workflows/dsl/DslTransformEditor.vue @@ -852,6 +852,7 @@ type: 'send_email', template_uuid: '', to: [], + cc: null, target_status: '', } } else { diff --git a/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts b/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts index 0b6c1849..eb9fba9d 100644 --- a/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts +++ b/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts @@ -11,7 +11,7 @@ vi.mock('@/api/typed-client', () => ({ getDslFromOptions: vi.fn().mockResolvedValue({}), getDslToOptions: vi.fn().mockResolvedValue({}), getDslTransformOptions: vi.fn().mockResolvedValue({}), - listEmailTemplates: (type?: string) => mockListEmailTemplates(type), + listEmailTemplates: (query?: { type?: string }) => mockListEmailTemplates(query), }, })) @@ -231,7 +231,7 @@ describe('PostRunActionsEditor', () => { await nextTick() await nextTick() - expect(mockListEmailTemplates).toHaveBeenCalledWith('workflow') + expect(mockListEmailTemplates).toHaveBeenCalledWith({ type: 'workflow' }) }) it('handles template loading failure gracefully', async () => { diff --git a/fe/src/components/workflows/dsl/PostRunActionsEditor.vue b/fe/src/components/workflows/dsl/PostRunActionsEditor.vue index 7e5a9e33..72bee452 100644 --- a/fe/src/components/workflows/dsl/PostRunActionsEditor.vue +++ b/fe/src/components/workflows/dsl/PostRunActionsEditor.vue @@ -195,7 +195,7 @@ async function loadEmailTemplates() { try { - const templates = await typedHttpClient.listEmailTemplates('workflow') + const templates = await typedHttpClient.listEmailTemplates({ type: 'workflow' }) emailTemplates.value = templates emailTemplateItems.value = templates.map(tmpl => ({ title: tmpl.name, diff --git a/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue b/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue index 4902faa7..97ee748c 100644 --- a/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue +++ b/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue @@ -228,7 +228,7 @@ async function loadEmailTemplates() { try { - const templates = await typedHttpClient.listEmailTemplates('workflow') + const templates = await typedHttpClient.listEmailTemplates({ type: 'workflow' }) emailTemplates.value = templates emailTemplateItems.value = templates.map(tmpl => ({ title: tmpl.name, @@ -333,7 +333,7 @@ const updated = existing.filter((_, i) => i !== idx) emit('update:modelValue', { ...props.modelValue, - cc: updated.length > 0 ? updated : undefined, + cc: updated.length > 0 ? updated : null, }) } } diff --git a/fe/src/components/workflows/dsl/templates.ts b/fe/src/components/workflows/dsl/templates.ts index ff01ca47..12b6dd74 100644 --- a/fe/src/components/workflows/dsl/templates.ts +++ b/fe/src/components/workflows/dsl/templates.ts @@ -109,6 +109,7 @@ export function createWorkflowTemplates(): WorkflowTemplate[] { type: 'send_email', template_uuid: '', to: [{ kind: 'field', field: 'email' }], + cc: null, target_status: 'email_status', }, to: { diff --git a/fe/src/composables/useApiKeys.test.ts b/fe/src/composables/useApiKeys.test.ts index 8a5d4527..a4d4c36e 100644 --- a/fe/src/composables/useApiKeys.test.ts +++ b/fe/src/composables/useApiKeys.test.ts @@ -98,8 +98,12 @@ describe('useApiKeys', () => { expect(loading.value).toBe(false) expect(apiKeys.value).toEqual(mockApiKeys) expect(mockGetApiKeys).toHaveBeenCalled() - expect(mockGetApiKeys.mock.calls[0][0]).toBe(1) - expect(mockGetApiKeys.mock.calls[0][1]).toBe(10) + expect(mockGetApiKeys.mock.calls[0][0]).toEqual({ + page: 1, + per_page: 10, + limit: null, + offset: null, + }) }) it('should handle loading errors', async () => { @@ -132,7 +136,11 @@ describe('useApiKeys', () => { describe('createApiKey', () => { it('should create API key successfully', async () => { - const request: CreateApiKeyRequest = { name: 'New Key' } + const request: CreateApiKeyRequest = { + name: 'New Key', + description: null, + expires_in_days: null, + } const result = { api_key: 'test_key_12345' } mockCreateApiKey.mockResolvedValue(result) mockGetApiKeys.mockResolvedValue({ @@ -150,7 +158,11 @@ describe('useApiKeys', () => { }) it('should handle creation errors', async () => { - const request: CreateApiKeyRequest = { name: 'New Key' } + const request: CreateApiKeyRequest = { + name: 'New Key', + description: null, + expires_in_days: null, + } const error = new Error('Creation failed') mockCreateApiKey.mockRejectedValue(error) @@ -220,8 +232,12 @@ describe('useApiKeys', () => { expect(currentPage.value).toBe(2) expect(mockGetApiKeys).toHaveBeenCalled() - expect(mockGetApiKeys.mock.calls[0][0]).toBe(2) - expect(mockGetApiKeys.mock.calls[0][1]).toBe(10) + expect(mockGetApiKeys.mock.calls[0][0]).toEqual({ + page: 2, + per_page: 10, + limit: null, + offset: null, + }) }) }) @@ -239,8 +255,12 @@ describe('useApiKeys', () => { expect(currentPage.value).toBe(1) expect(mockGetApiKeys).toHaveBeenCalled() const lastCall = mockGetApiKeys.mock.calls[mockGetApiKeys.mock.calls.length - 1] - expect(lastCall[0]).toBe(1) - expect(lastCall[1]).toBe(25) + expect(lastCall[0]).toEqual({ + page: 1, + per_page: 25, + limit: null, + offset: null, + }) }) }) }) diff --git a/fe/src/composables/useApiKeys.ts b/fe/src/composables/useApiKeys.ts index 72d9be81..f028d745 100644 --- a/fe/src/composables/useApiKeys.ts +++ b/fe/src/composables/useApiKeys.ts @@ -3,7 +3,7 @@ import { typedHttpClient } from '@/api/typed-client' import { useErrorHandler } from './useErrorHandler' import { useTranslations } from './useTranslations' import { useAuthStore } from '@/stores/auth' -import type { ApiKey, CreateApiKeyRequest } from '@/types/schemas' +import type { ApiKey, CreateApiKeyRequest, PaginationMeta } from '@/types/schemas' export function useApiKeys() { const { handleError, handleSuccess } = useErrorHandler() @@ -20,14 +20,7 @@ export function useApiKeys() { const itemsPerPage = ref(10) const totalItems = ref(0) const totalPages = ref(1) - const paginationMeta = ref<{ - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean - } | null>(null) + const paginationMeta = ref(null) /** * Load API keys with pagination @@ -37,7 +30,12 @@ export function useApiKeys() { error.value = '' try { - const response = await typedHttpClient.getApiKeys(page, perPage) + const response = await typedHttpClient.getApiKeys({ + page, + per_page: perPage, + limit: null, + offset: null, + }) apiKeys.value = response.data if (response.meta?.pagination) { totalItems.value = response.meta.pagination.total diff --git a/fe/src/composables/useEntities.test.ts b/fe/src/composables/useEntities.test.ts index 63daf3d8..221e2c47 100644 --- a/fe/src/composables/useEntities.test.ts +++ b/fe/src/composables/useEntities.test.ts @@ -160,7 +160,7 @@ describe('useEntities', () => { data: { name: '' }, } const validationError = new ValidationError('Validation failed', [ - { field: 'name', message: 'Required' }, + { field: 'name', message: 'Required', code: null }, ]) mockCreateEntity.mockRejectedValue(validationError) diff --git a/fe/src/composables/useEntities.ts b/fe/src/composables/useEntities.ts index 678f90dd..011275b1 100644 --- a/fe/src/composables/useEntities.ts +++ b/fe/src/composables/useEntities.ts @@ -30,7 +30,12 @@ export function useEntities() { */ const loadEntityDefinitions = async (): Promise => { try { - const response = await typedHttpClient.getEntityDefinitions() + const response = await typedHttpClient.getEntityDefinitions({ + page: null, + per_page: null, + limit: null, + offset: null, + }) entityDefinitions.value = response.data } catch (err) { handleError(err, 'Failed to load entity definitions') diff --git a/fe/src/composables/useEntityDefinitions.ts b/fe/src/composables/useEntityDefinitions.ts index ebd05553..2ac4718b 100644 --- a/fe/src/composables/useEntityDefinitions.ts +++ b/fe/src/composables/useEntityDefinitions.ts @@ -51,7 +51,12 @@ export function useEntityDefinitions() { error.value = '' try { - const response = await typedHttpClient.getEntityDefinitions() + const response = await typedHttpClient.getEntityDefinitions({ + page: null, + per_page: null, + limit: null, + offset: null, + }) entityDefinitions.value = response.data.map(definition => ({ ...definition, fields: sanitizeFields(definition.fields), diff --git a/fe/src/composables/useErrorHandler.test.ts b/fe/src/composables/useErrorHandler.test.ts index 04505c25..8274f38b 100644 --- a/fe/src/composables/useErrorHandler.test.ts +++ b/fe/src/composables/useErrorHandler.test.ts @@ -34,8 +34,8 @@ describe('useErrorHandler', () => { it('should handle ValidationError with field errors', () => { const { handleError } = useErrorHandler() const violations = [ - { field: 'name', message: 'Name is required' }, - { field: 'email', message: 'Email is invalid' }, + { field: 'name', message: 'Name is required', code: null }, + { field: 'email', message: 'Email is invalid', code: null }, ] const error = new ValidationError('Validation failed', violations) @@ -288,8 +288,8 @@ describe('useErrorHandler', () => { it('should extract field errors from ValidationError', () => { const { extractFieldErrors } = useErrorHandler() const violations = [ - { field: 'name', message: 'Name is required' }, - { field: 'email', message: 'Email is invalid' }, + { field: 'name', message: 'Name is required', code: null }, + { field: 'email', message: 'Email is invalid', code: null }, ] const error = new ValidationError('Validation failed', violations) diff --git a/fe/src/composables/useErrorHandler.ts b/fe/src/composables/useErrorHandler.ts index 16528eac..df4ff2b3 100644 --- a/fe/src/composables/useErrorHandler.ts +++ b/fe/src/composables/useErrorHandler.ts @@ -3,12 +3,12 @@ import { useTranslations } from './useTranslations' import { ValidationError } from '@/api/typed-client' import { HttpError } from '@/api/errors' -export interface FieldError { - field: string - message: string - code?: string -} - +/** + * FE-only return shape of `handleError` — not a BE payload. + * `handled` lets callers know the error was already surfaced (snackbar etc.). + * `fieldErrors` is a UI-friendly `field → message` map derived from the BE's + * `ValidationViolation[]` (generated) so form components can display per-field errors. + */ export interface ErrorHandlerResult { handled: boolean fieldErrors?: Record diff --git a/fe/src/composables/useRoles.test.ts b/fe/src/composables/useRoles.test.ts index 0bea7ab6..795717e9 100644 --- a/fe/src/composables/useRoles.test.ts +++ b/fe/src/composables/useRoles.test.ts @@ -64,13 +64,19 @@ describe('useRoles', () => { has_previous: false, has_next: false, }, + request_id: null, + timestamp: null, + custom: null, }, }) const { loadRoles, roles } = useRoles() await loadRoles() - expect(typedHttpClient.getRoles).toHaveBeenCalledWith(1, 20, undefined, undefined) + expect(typedHttpClient.getRoles).toHaveBeenCalledWith( + { page: 1, per_page: 20, limit: null, offset: null }, + null + ) expect(roles.value).toEqual(mockRoles) }) @@ -110,6 +116,7 @@ describe('useRoles', () => { const updateData: UpdateRoleRequest = { name: 'Updated Role', description: 'Updated description', + super_admin: null, permissions: [], } @@ -175,6 +182,7 @@ describe('useRoles', () => { const updateData: UpdateRoleRequest = { name: 'Updated Role', description: 'Updated description', + super_admin: null, permissions: [], } @@ -221,9 +229,10 @@ describe('useRoles', () => { await expect( createRole({ name: 'Test Role', + description: null, permissions: [], super_admin: false, - } as CreateRoleRequest) + }) ).rejects.toThrow('HTTP 403: Forbidden') expect(mockHandleError).toHaveBeenCalled() }) diff --git a/fe/src/composables/useRoles.ts b/fe/src/composables/useRoles.ts index ba3b4ada..431cab3b 100644 --- a/fe/src/composables/useRoles.ts +++ b/fe/src/composables/useRoles.ts @@ -22,7 +22,10 @@ export function useRoles() { error.value = '' try { - const response = await typedHttpClient.getRoles(page, perPage, sortBy, sortOrder) + const response = await typedHttpClient.getRoles( + { page, per_page: perPage, limit: null, offset: null }, + sortBy && sortOrder ? { sort_by: sortBy, sort_order: sortOrder } : null + ) roles.value = response.data return response } catch (err) { diff --git a/fe/src/composables/useUsers.test.ts b/fe/src/composables/useUsers.test.ts index e4d821e2..56a82873 100644 --- a/fe/src/composables/useUsers.test.ts +++ b/fe/src/composables/useUsers.test.ts @@ -70,13 +70,19 @@ describe('useUsers', () => { has_previous: false, has_next: false, }, + request_id: null, + timestamp: null, + custom: null, }, }) const { loadUsers, users } = useUsers() await loadUsers() - expect(typedHttpClient.getUsers).toHaveBeenCalledWith(1, 20, undefined, undefined) + expect(typedHttpClient.getUsers).toHaveBeenCalledWith( + { page: 1, per_page: 20, limit: null, offset: null }, + null + ) expect(users.value).toEqual(mockUsers) }) @@ -87,6 +93,7 @@ describe('useUsers', () => { password: 'password123', first_name: 'New', last_name: 'User', + role_uuids: null, is_active: true, super_admin: false, } @@ -121,8 +128,11 @@ describe('useUsers', () => { it('should update a user successfully', async () => { const updateData: UpdateUserRequest = { email: 'updated@example.com', + password: null, first_name: 'Updated', last_name: 'Name', + role_uuids: null, + is_active: null, super_admin: true, } @@ -175,6 +185,7 @@ describe('useUsers', () => { password: 'password123', first_name: 'New', last_name: 'User', + role_uuids: null, is_active: true, super_admin: false, } @@ -191,8 +202,12 @@ describe('useUsers', () => { it('should handle 403 error when updating user', async () => { const updateData: UpdateUserRequest = { email: 'updated@example.com', + password: null, first_name: 'Updated', last_name: 'Name', + role_uuids: null, + is_active: null, + super_admin: null, } const error = new Error('HTTP 403: Forbidden') @@ -277,6 +292,7 @@ describe('useUsers', () => { password: 'password123', first_name: 'New', last_name: 'User', + role_uuids: null, is_active: true, super_admin: false, } @@ -311,6 +327,12 @@ describe('useUsers', () => { it('should show success message when updating user', async () => { const updateData: UpdateUserRequest = { email: 'updated@example.com', + password: null, + first_name: null, + last_name: null, + role_uuids: null, + is_active: null, + super_admin: null, } const mockUser: UserResponse = { diff --git a/fe/src/composables/useUsers.ts b/fe/src/composables/useUsers.ts index 615466d2..5c2ff544 100644 --- a/fe/src/composables/useUsers.ts +++ b/fe/src/composables/useUsers.ts @@ -22,7 +22,10 @@ export function useUsers() { error.value = '' try { - const response = await typedHttpClient.getUsers(page, perPage, sortBy, sortOrder) + const response = await typedHttpClient.getUsers( + { page, per_page: perPage, limit: null, offset: null }, + sortBy && sortOrder ? { sort_by: sortBy, sort_order: sortOrder } : null + ) users.value = response.data return response } catch (err) { diff --git a/fe/src/pages/api-keys/ApiKeysPage.vue b/fe/src/pages/api-keys/ApiKeysPage.vue index 6cf31f84..d1ac06cc 100644 --- a/fe/src/pages/api-keys/ApiKeysPage.vue +++ b/fe/src/pages/api-keys/ApiKeysPage.vue @@ -328,10 +328,10 @@ `Loading API keys: page=${page}, itemsPerPage=${itemsPerPage}, sortBy=${sortBy.value}, sortOrder=${sortOrder.value}` ) const response = await typedHttpClient.getApiKeys( - page, - itemsPerPage, - sortBy.value, - sortOrder.value + { page, per_page: itemsPerPage, limit: null, offset: null }, + sortBy.value && sortOrder.value + ? { sort_by: sortBy.value, sort_order: sortOrder.value } + : null ) apiKeys.value = response.data if (response.meta?.pagination) { diff --git a/fe/src/pages/entities/EntitiesPage.vue b/fe/src/pages/entities/EntitiesPage.vue index 9f570b67..aa60666e 100644 --- a/fe/src/pages/entities/EntitiesPage.vue +++ b/fe/src/pages/entities/EntitiesPage.vue @@ -215,7 +215,12 @@ } try { - const response = await typedHttpClient.getEntityDefinitions() + const response = await typedHttpClient.getEntityDefinitions({ + page: null, + per_page: null, + limit: null, + offset: null, + }) entityDefinitions.value = response.data } catch (err) { console.error('Failed to load entity definitions:', err) diff --git a/fe/src/pages/entity-definitions/EntityDefinitionsPage.vue b/fe/src/pages/entity-definitions/EntityDefinitionsPage.vue index 33198784..61fefbdf 100644 --- a/fe/src/pages/entity-definitions/EntityDefinitionsPage.vue +++ b/fe/src/pages/entity-definitions/EntityDefinitionsPage.vue @@ -239,7 +239,12 @@ error.value = '' try { - const response = await typedHttpClient.getEntityDefinitions() + const response = await typedHttpClient.getEntityDefinitions({ + page: null, + per_page: null, + limit: null, + offset: null, + }) // Sanitize fields to ensure constraints and ui_settings are always objects entityDefinitions.value = response.data.map(definition => ({ ...definition, diff --git a/fe/src/pages/workflows/WorkflowsPage.vue b/fe/src/pages/workflows/WorkflowsPage.vue index 148030ab..46fa12f1 100644 --- a/fe/src/pages/workflows/WorkflowsPage.vue +++ b/fe/src/pages/workflows/WorkflowsPage.vue @@ -106,10 +106,10 @@ error.value = '' try { const response = await typedHttpClient.getWorkflows( - page, - perPage, - sortBy.value, - sortOrder.value + { page, per_page: perPage, limit: null, offset: null }, + sortBy.value && sortOrder.value + ? { sort_by: sortBy.value, sort_order: sortOrder.value } + : null ) // Normalize kind from API response (Consumer/Provider) to lowercase (consumer/provider) items.value = response.data.map(item => ({ @@ -250,13 +250,18 @@ } runsLoading.value = true try { + const runsPagination = { + page: runsPage.value, + per_page: runsPerPage.value, + limit: null, + offset: null, + } const res = selectedWorkflowUuid.value === 'all' - ? await typedHttpClient.getAllWorkflowRuns(runsPage.value, runsPerPage.value) + ? await typedHttpClient.getAllWorkflowRuns(runsPagination) : await typedHttpClient.getWorkflowRuns( selectedWorkflowUuid.value, - runsPage.value, - runsPerPage.value + runsPagination ) runs.value = res.data runsTotal.value = res.meta?.pagination?.total ?? res.data.length @@ -278,11 +283,12 @@ } logsLoading.value = true try { - const res = await typedHttpClient.getWorkflowRunLogs( - currentRunUuid.value, - logsPage.value, - logsPerPage.value - ) + const res = await typedHttpClient.getWorkflowRunLogs(currentRunUuid.value, { + page: logsPage.value, + per_page: logsPerPage.value, + limit: null, + offset: null, + }) logs.value = res.data logsTotal.value = res.meta?.pagination?.total ?? res.data.length } finally { diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index f2da8c30..3087f9e0 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -7,7 +7,26 @@ import { getRefreshToken, setRefreshToken, deleteRefreshToken } from '@/utils/co import { useLicenseStore } from './license' import { useVersionStore } from './versions' import { useCapabilitiesStore } from './capabilities' -import type { LoginRequest, User } from '@/types/schemas' +import type { LoginRequest } from '@/types/schemas' + +/** + * FE-only session shape synthesised from JWT claims (sub, username, email, roles). + * This is NOT a BE response type — there's currently no /auth/me endpoint returning + * a user DTO after login/refresh. When such an endpoint is added, replace this with + * `UserResponse` from `@/types/generated/UserResponse` and drop the manual fields. + */ +interface AuthUser { + uuid: string + username: string + email: string + first_name: string + last_name: string + role_uuids: string[] + is_active: boolean + is_admin: boolean + created_at: string + updated_at: string +} export const useAuthStore = defineStore('auth', () => { // Translation system @@ -15,7 +34,7 @@ export const useAuthStore = defineStore('auth', () => { // State - access token only in memory, refresh token in secure cookie const access_token = ref(null) - const user = ref(null) + const user = ref(null) const refreshTimer = ref | null>(null) const isLoading = ref(false) const error = ref(null) diff --git a/fe/src/stores/license.test.ts b/fe/src/stores/license.test.ts index 395d120b..145e73de 100644 --- a/fe/src/stores/license.test.ts +++ b/fe/src/stores/license.test.ts @@ -30,6 +30,7 @@ describe('LicenseStore', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: null, @@ -52,6 +53,7 @@ describe('LicenseStore', () => { license_type: null, license_id: null, issued_at: null, + expires_at: null, version: null, verified_at: '2024-01-02T00:00:00Z', error_message: null, @@ -72,6 +74,7 @@ describe('LicenseStore', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: 'Invalid license', @@ -92,6 +95,7 @@ describe('LicenseStore', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: 'Network error', @@ -112,6 +116,7 @@ describe('LicenseStore', () => { license_type: 'Enterprise', license_id: 'test-id', issued_at: '2024-01-01T00:00:00Z', + expires_at: null, version: 'v1', verified_at: '2024-01-02T00:00:00Z', error_message: null, @@ -132,6 +137,7 @@ describe('LicenseStore', () => { license_type: null, license_id: null, issued_at: null, + expires_at: null, version: null, verified_at: '2024-01-02T00:00:00Z', error_message: null, diff --git a/fe/src/stores/versions.test.ts b/fe/src/stores/versions.test.ts index 87d7067b..da767729 100644 --- a/fe/src/stores/versions.test.ts +++ b/fe/src/stores/versions.test.ts @@ -66,8 +66,8 @@ describe('VersionStore', () => { it('should load versions with only core', async () => { const mockVersions: SystemVersions = { core: '2.0.0', - worker: undefined, - maintenance: undefined, + worker: null, + maintenance: null, } vi.mocked(typedHttpClient.getSystemVersions).mockResolvedValue(mockVersions) diff --git a/fe/src/types/common.ts b/fe/src/types/common.ts index 9cea00bd..7c2cf910 100644 --- a/fe/src/types/common.ts +++ b/fe/src/types/common.ts @@ -1,5 +1,6 @@ /** - * Common type definitions used across the application + * Common UI-only type definitions used across the application. + * Payload shapes from the backend live in `@/types/generated` and must not be duplicated here. */ /** @@ -10,18 +11,6 @@ export type TableRow = Record uuid: string } & T -/** - * Pagination metadata structure - */ -export interface PaginationMeta { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean -} - /** * Table header definition */ @@ -32,14 +21,3 @@ export interface TableHeader { align?: 'start' | 'center' | 'end' width?: string } - -/** - * Table action definition - */ -export interface TableAction { - icon: string - color?: string - disabled?: boolean - loading?: boolean - onClick?: (item: TableRow) => void -} diff --git a/fe/src/types/generated/AccessLevel.ts b/fe/src/types/generated/AccessLevel.ts index 04f32666..875b6a0d 100644 --- a/fe/src/types/generated/AccessLevel.ts +++ b/fe/src/types/generated/AccessLevel.ts @@ -3,4 +3,4 @@ /** * Access level for a permission */ -export type AccessLevel = "None" | "Own" | "Group" | "All"; +export type AccessLevel = 'None' | 'Own' | 'Group' | 'All' diff --git a/fe/src/types/generated/AdminLoginRequest.ts b/fe/src/types/generated/AdminLoginRequest.ts index 8b84dec9..023c4a0c 100644 --- a/fe/src/types/generated/AdminLoginRequest.ts +++ b/fe/src/types/generated/AdminLoginRequest.ts @@ -3,12 +3,13 @@ /** * Admin login request body */ -export type AdminLoginRequest = { -/** - * Username or email - */ -username: string, -/** - * Password - */ -password: string, }; +export type AdminLoginRequest = { + /** + * Username or email + */ + username: string + /** + * Password + */ + password: string +} diff --git a/fe/src/types/generated/AdminLoginResponse.ts b/fe/src/types/generated/AdminLoginResponse.ts index 154d4796..750cd008 100644 --- a/fe/src/types/generated/AdminLoginResponse.ts +++ b/fe/src/types/generated/AdminLoginResponse.ts @@ -3,32 +3,33 @@ /** * Admin login response body */ -export type AdminLoginResponse = { -/** - * JWT access token - */ -access_token: string, -/** - * Refresh token - */ -refresh_token: string, -/** - * User UUID - */ -user_uuid: string, -/** - * Username - */ -username: string, -/** - * Access token expiration (RFC3339 timestamp) - */ -access_expires_at: string, -/** - * Refresh token expiration (RFC3339 timestamp) - */ -refresh_expires_at: string, -/** - * Whether the default admin password is still in use (false if check is disabled) - */ -using_default_password: boolean, }; +export type AdminLoginResponse = { + /** + * JWT access token + */ + access_token: string + /** + * Refresh token + */ + refresh_token: string + /** + * User UUID + */ + user_uuid: string + /** + * Username + */ + username: string + /** + * Access token expiration (RFC3339 timestamp) + */ + access_expires_at: string + /** + * Refresh token expiration (RFC3339 timestamp) + */ + refresh_expires_at: string + /** + * Whether the default admin password is still in use (false if check is disabled) + */ + using_default_password: boolean +} diff --git a/fe/src/types/generated/AdminRegisterRequest.ts b/fe/src/types/generated/AdminRegisterRequest.ts deleted file mode 100644 index 9dc4fb57..00000000 --- a/fe/src/types/generated/AdminRegisterRequest.ts +++ /dev/null @@ -1,30 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Admin registration request body - */ -export type AdminRegisterRequest = { -/** - * Username - */ -username: string, -/** - * Email - */ -email: string, -/** - * Password - */ -password: string, -/** - * First name - */ -first_name: string, -/** - * Last name - */ -last_name: string, -/** - * User role - */ -role: string | null, }; diff --git a/fe/src/types/generated/AdminRegisterResponse.ts b/fe/src/types/generated/AdminRegisterResponse.ts deleted file mode 100644 index ff4ecd9d..00000000 --- a/fe/src/types/generated/AdminRegisterResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Admin registration response body - */ -export type AdminRegisterResponse = { -/** - * User UUID - */ -uuid: string, -/** - * Username - */ -username: string, -/** - * Message - */ -message: string, }; diff --git a/fe/src/types/generated/ApiKeyCreatedResponse.ts b/fe/src/types/generated/ApiKeyCreatedResponse.ts index 32a48dcb..a2a54114 100644 --- a/fe/src/types/generated/ApiKeyCreatedResponse.ts +++ b/fe/src/types/generated/ApiKeyCreatedResponse.ts @@ -3,48 +3,49 @@ /** * Response when an API key is created (includes the actual key value) */ -export type ApiKeyCreatedResponse = { -/** - * UUID of the API key - */ -uuid: string, -/** - * Name of the API key - */ -name: string, -/** - * The actual API key value (only shown once at creation) - */ -api_key: string, -/** - * Description of the API key - */ -description: string | null, -/** - * Whether the API key is active - */ -is_active: boolean, -/** - * When the API key was created - */ -created_at: string, -/** - * When the API key expires (if applicable) - */ -expires_at: string | null, -/** - * UUID of the user who created this key - */ -created_by: string, -/** - * UUID of the user to whom this key is assigned - */ -user_uuid: string, -/** - * Whether the key is published - */ -published: boolean, -/** - * When the API key was last used - */ -last_used_at: string | null, }; +export type ApiKeyCreatedResponse = { + /** + * UUID of the API key + */ + uuid: string + /** + * Name of the API key + */ + name: string + /** + * The actual API key value (only shown once at creation) + */ + api_key: string + /** + * Description of the API key + */ + description: string | null + /** + * Whether the API key is active + */ + is_active: boolean + /** + * When the API key was created + */ + created_at: string + /** + * When the API key expires (if applicable) + */ + expires_at: string | null + /** + * UUID of the user who created this key + */ + created_by: string + /** + * UUID of the user to whom this key is assigned + */ + user_uuid: string + /** + * Whether the key is published + */ + published: boolean + /** + * When the API key was last used + */ + last_used_at: string | null +} diff --git a/fe/src/types/generated/ApiKeyResponse.ts b/fe/src/types/generated/ApiKeyResponse.ts index 61e912de..5a18b768 100644 --- a/fe/src/types/generated/ApiKeyResponse.ts +++ b/fe/src/types/generated/ApiKeyResponse.ts @@ -3,44 +3,45 @@ /** * Response containing API key information */ -export type ApiKeyResponse = { -/** - * UUID of the API key - */ -uuid: string, -/** - * Name of the API key - */ -name: string, -/** - * Description of the API key - */ -description: string | null, -/** - * Whether the API key is active - */ -is_active: boolean, -/** - * When the API key was created - */ -created_at: string, -/** - * When the API key expires (if applicable) - */ -expires_at: string | null, -/** - * When the API key was last used - */ -last_used_at: string | null, -/** - * UUID of the user who created this key - */ -created_by: string, -/** - * UUID of the user to whom this key is assigned - */ -user_uuid: string, -/** - * Whether the key is published - */ -published: boolean, }; +export type ApiKeyResponse = { + /** + * UUID of the API key + */ + uuid: string + /** + * Name of the API key + */ + name: string + /** + * Description of the API key + */ + description: string | null + /** + * Whether the API key is active + */ + is_active: boolean + /** + * When the API key was created + */ + created_at: string + /** + * When the API key expires (if applicable) + */ + expires_at: string | null + /** + * When the API key was last used + */ + last_used_at: string | null + /** + * UUID of the user who created this key + */ + created_by: string + /** + * UUID of the user to whom this key is assigned + */ + user_uuid: string + /** + * Whether the key is published + */ + published: boolean +} diff --git a/fe/src/types/generated/ApplySchemaRequest.ts b/fe/src/types/generated/ApplySchemaRequest.ts index f9b4d223..4bf18ff7 100644 --- a/fe/src/types/generated/ApplySchemaRequest.ts +++ b/fe/src/types/generated/ApplySchemaRequest.ts @@ -4,9 +4,10 @@ * Model for apply-schema request * Used to generate and apply SQL schema for a specific entity definition or all definitions */ -export type ApplySchemaRequest = { -/** - * Optional UUID of specific entity definition to apply schema for - * If not provided, schemas for all published entity definitions will be applied - */ -uuid: string | null, }; +export type ApplySchemaRequest = { + /** + * Optional UUID of specific entity definition to apply schema for + * If not provided, schemas for all published entity definitions will be applied + */ + uuid: string | null +} diff --git a/fe/src/types/generated/AssignRolesRequest.ts b/fe/src/types/generated/AssignRolesRequest.ts index 49973a0d..e1a71a26 100644 --- a/fe/src/types/generated/AssignRolesRequest.ts +++ b/fe/src/types/generated/AssignRolesRequest.ts @@ -3,8 +3,9 @@ /** * Request to assign roles to a user or API key */ -export type AssignRolesRequest = { -/** - * UUIDs of roles to assign - */ -role_uuids: string[], }; +export type AssignRolesRequest = { + /** + * UUIDs of roles to assign + */ + role_uuids: string[] +} diff --git a/fe/src/types/generated/EmptyRequest.ts b/fe/src/types/generated/BrowseKind.ts similarity index 50% rename from fe/src/types/generated/EmptyRequest.ts rename to fe/src/types/generated/BrowseKind.ts index ebb9c59f..e586495b 100644 --- a/fe/src/types/generated/EmptyRequest.ts +++ b/fe/src/types/generated/BrowseKind.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. /** - * Empty request body for endpoints that don't require any input + * Kind of browse node */ -export type EmptyRequest = Record; +export type BrowseKind = 'folder' | 'file' diff --git a/fe/src/types/generated/BrowseNode.ts b/fe/src/types/generated/BrowseNode.ts new file mode 100644 index 00000000..50a6f325 --- /dev/null +++ b/fe/src/types/generated/BrowseNode.ts @@ -0,0 +1,36 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BrowseKind } from './BrowseKind' + +/** + * Node returned when browsing entities by virtual path + */ +export type BrowseNode = { + /** + * "folder" or "file" + */ + kind: BrowseKind + /** + * Item name (folder segment or file name) + */ + name: string + /** + * Full path for this item + */ + path: string + /** + * Present for files or folder-entities that exist as entities + */ + entity_uuid: string | null + /** + * Type of the entity if present + */ + entity_type: string | null + /** + * Whether the folder has children (only meaningful when kind = folder) + */ + has_children: boolean | null + /** + * Whether the entity is published (only meaningful when kind = file) + */ + published: boolean +} diff --git a/fe/src/types/generated/CapabilitiesResponse.ts b/fe/src/types/generated/CapabilitiesResponse.ts index d5f31cdb..f5aec8a1 100644 --- a/fe/src/types/generated/CapabilitiesResponse.ts +++ b/fe/src/types/generated/CapabilitiesResponse.ts @@ -3,12 +3,13 @@ /** * Response for system capabilities (which optional features are configured) */ -export type CapabilitiesResponse = { -/** - * Whether system mail is configured (enables password reset etc.) - */ -system_mail_configured: boolean, -/** - * Whether workflow mail is configured (enables email outputs in workflows) - */ -workflow_mail_configured: boolean, }; +export type CapabilitiesResponse = { + /** + * Whether system mail is configured (enables password reset etc.) + */ + system_mail_configured: boolean + /** + * Whether workflow mail is configured (enables email outputs in workflows) + */ + workflow_mail_configured: boolean +} diff --git a/fe/src/types/generated/ComponentVersionDto.ts b/fe/src/types/generated/ComponentVersionDto.ts new file mode 100644 index 00000000..3ef81765 --- /dev/null +++ b/fe/src/types/generated/ComponentVersionDto.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Component version information + */ +export type ComponentVersionDto = { + /** + * Name of the component + */ + name: string + /** + * Version string + */ + version: string + /** + * Last time this component was seen (ISO 8601) + */ + last_seen_at: string +} diff --git a/fe/src/types/generated/CreateApiKeyRequest.ts b/fe/src/types/generated/CreateApiKeyRequest.ts index e4dc9fab..43db8cf2 100644 --- a/fe/src/types/generated/CreateApiKeyRequest.ts +++ b/fe/src/types/generated/CreateApiKeyRequest.ts @@ -3,16 +3,17 @@ /** * Request to create a new API key */ -export type CreateApiKeyRequest = { -/** - * Name of the API key - */ -name: string, -/** - * Optional description for the API key - */ -description: string | null, -/** - * Number of days until expiration (default: 365) - */ -expires_in_days: number | null, }; +export type CreateApiKeyRequest = { + /** + * Name of the API key + */ + name: string + /** + * Optional description for the API key + */ + description: string | null + /** + * Number of days until expiration (default: 365) + */ + expires_in_days: number | null +} diff --git a/fe/src/types/generated/CreateEmailTemplateRequest.ts b/fe/src/types/generated/CreateEmailTemplateRequest.ts index 7636b103..a5e89f86 100644 --- a/fe/src/types/generated/CreateEmailTemplateRequest.ts +++ b/fe/src/types/generated/CreateEmailTemplateRequest.ts @@ -3,28 +3,29 @@ /** * Request body for creating a new email template */ -export type CreateEmailTemplateRequest = { -/** - * Display name for the template - */ -name: string, -/** - * Unique slug identifier - */ -slug: string, -/** - * Subject line (may contain template variables) - */ -subject_template: string, -/** - * HTML body (may contain template variables) - */ -body_html_template: string, -/** - * Plain-text body (may contain template variables) - */ -body_text_template: string, -/** - * JSON object describing available template variables - */ -variables: unknown, }; +export type CreateEmailTemplateRequest = { + /** + * Display name for the template + */ + name: string + /** + * Unique slug identifier + */ + slug: string + /** + * Subject line (may contain template variables) + */ + subject_template: string + /** + * HTML body (may contain template variables) + */ + body_html_template: string + /** + * Plain-text body (may contain template variables) + */ + body_text_template: string + /** + * JSON object describing available template variables + */ + variables: unknown +} diff --git a/fe/src/types/generated/CreateRoleRequest.ts b/fe/src/types/generated/CreateRoleRequest.ts index 7ba9d496..69af0585 100644 --- a/fe/src/types/generated/CreateRoleRequest.ts +++ b/fe/src/types/generated/CreateRoleRequest.ts @@ -1,23 +1,24 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionResponse } from "./PermissionResponse"; +import type { PermissionResponse } from './PermissionResponse' /** * Request to create a new role */ -export type CreateRoleRequest = { -/** - * Name of the role - */ -name: string, -/** - * Optional description - */ -description: string | null, -/** - * Whether this role grants super admin privileges - */ -super_admin: boolean | null, -/** - * Direct permissions for this role - */ -permissions: Array, }; +export type CreateRoleRequest = { + /** + * Name of the role + */ + name: string + /** + * Optional description + */ + description: string | null + /** + * Whether this role grants super admin privileges + */ + super_admin: boolean | null + /** + * Direct permissions for this role + */ + permissions: Array +} diff --git a/fe/src/types/generated/CreateUserRequest.ts b/fe/src/types/generated/CreateUserRequest.ts index ea305abf..a4095368 100644 --- a/fe/src/types/generated/CreateUserRequest.ts +++ b/fe/src/types/generated/CreateUserRequest.ts @@ -3,36 +3,37 @@ /** * Create user request */ -export type CreateUserRequest = { -/** - * Username - */ -username: string, -/** - * Email address - */ -email: string, -/** - * Password - */ -password: string, -/** - * First name - */ -first_name: string, -/** - * Last name - */ -last_name: string, -/** - * Role UUIDs to assign to this user (optional) - */ -role_uuids: string[] | null, -/** - * Whether user is active - */ -is_active: boolean | null, -/** - * Super admin flag - */ -super_admin: boolean | null, }; +export type CreateUserRequest = { + /** + * Username + */ + username: string + /** + * Email address + */ + email: string + /** + * Password + */ + password: string + /** + * First name + */ + first_name: string + /** + * Last name + */ + last_name: string + /** + * Role UUIDs to assign to this user (optional) + */ + role_uuids: string[] | null + /** + * Whether user is active + */ + is_active: boolean | null + /** + * Super admin flag + */ + super_admin: boolean | null +} diff --git a/fe/src/types/generated/CreateWorkflowRequest.ts b/fe/src/types/generated/CreateWorkflowRequest.ts new file mode 100644 index 00000000..57f9353d --- /dev/null +++ b/fe/src/types/generated/CreateWorkflowRequest.ts @@ -0,0 +1,35 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Request to create a new workflow + */ +export type CreateWorkflowRequest = { + /** + * Workflow name + */ + name: string + /** + * Workflow description + */ + description: string | null + /** + * Workflow kind (consumer or provider) + */ + kind: string + /** + * Whether the workflow is enabled + */ + enabled: boolean + /** + * Cron schedule for the workflow + */ + schedule_cron: string | null + /** + * Workflow configuration + */ + config: unknown + /** + * Whether versioning is disabled + */ + versioning_disabled: boolean +} diff --git a/fe/src/types/generated/CreateWorkflowResponse.ts b/fe/src/types/generated/CreateWorkflowResponse.ts index fff45c54..dc3b9693 100644 --- a/fe/src/types/generated/CreateWorkflowResponse.ts +++ b/fe/src/types/generated/CreateWorkflowResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CreateWorkflowResponse = { uuid: string, }; +export type CreateWorkflowResponse = { uuid: string } diff --git a/fe/src/types/generated/DashboardStats.ts b/fe/src/types/generated/DashboardStats.ts new file mode 100644 index 00000000..c1cccc97 --- /dev/null +++ b/fe/src/types/generated/DashboardStats.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EntityStats } from './EntityStats' +import type { WorkflowStats } from './WorkflowStats' + +/** + * Dashboard statistics response + */ +export type DashboardStats = { + /** + * Total count of entity definitions + */ + entity_definitions_count: number + /** + * Entity statistics + */ + entities: EntityStats + /** + * Workflow statistics + */ + workflows: WorkflowStats + /** + * Count of online users (users with active refresh tokens) + */ + online_users_count: number +} diff --git a/fe/src/types/generated/DateTimeConstraints.ts b/fe/src/types/generated/DateTimeConstraints.ts index ab4abf47..3aa775c9 100644 --- a/fe/src/types/generated/DateTimeConstraints.ts +++ b/fe/src/types/generated/DateTimeConstraints.ts @@ -3,12 +3,13 @@ /** * Date/time field constraints */ -export type DateTimeConstraints = { -/** - * Minimum allowed date - */ -min_date: string | null, -/** - * Maximum allowed date - */ -max_date: string | null, }; +export type DateTimeConstraints = { + /** + * Minimum allowed date + */ + min_date: string | null + /** + * Maximum allowed date + */ + max_date: string | null +} diff --git a/fe/src/types/generated/DslFieldSpec.ts b/fe/src/types/generated/DslFieldSpec.ts index f92c2f86..338ed358 100644 --- a/fe/src/types/generated/DslFieldSpec.ts +++ b/fe/src/types/generated/DslFieldSpec.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DslFieldSpec = { name: string, type: string, required: boolean, options: Array | null, }; +export type DslFieldSpec = { + name: string + type: string + required: boolean + options: Array | null +} diff --git a/fe/src/types/generated/DslOptionsAndExamplesResponse.ts b/fe/src/types/generated/DslOptionsAndExamplesResponse.ts index 4f7aa1f3..29de3b94 100644 --- a/fe/src/types/generated/DslOptionsAndExamplesResponse.ts +++ b/fe/src/types/generated/DslOptionsAndExamplesResponse.ts @@ -1,8 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DslTypeSpec } from "./DslTypeSpec"; +import type { DslTypeSpec } from './DslTypeSpec' -export type DslOptionsAndExamplesResponse = { types: Array, -/** - * Concrete serialized examples using the real DSL structs - */ -examples: unknown[], }; +export type DslOptionsAndExamplesResponse = { + types: Array + /** + * Concrete serialized examples using the real DSL structs + */ + examples: unknown[] +} diff --git a/fe/src/types/generated/DslOptionsResponse.ts b/fe/src/types/generated/DslOptionsResponse.ts index 962f1502..69a04926 100644 --- a/fe/src/types/generated/DslOptionsResponse.ts +++ b/fe/src/types/generated/DslOptionsResponse.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DslTypeSpec } from "./DslTypeSpec"; +import type { DslTypeSpec } from './DslTypeSpec' -export type DslOptionsResponse = { types: Array, }; +export type DslOptionsResponse = { types: Array } diff --git a/fe/src/types/generated/DslTypeSpec.ts b/fe/src/types/generated/DslTypeSpec.ts index b8cd393b..4a6bde2b 100644 --- a/fe/src/types/generated/DslTypeSpec.ts +++ b/fe/src/types/generated/DslTypeSpec.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DslFieldSpec } from "./DslFieldSpec"; +import type { DslFieldSpec } from './DslFieldSpec' -export type DslTypeSpec = { type: string, fields: Array, }; +export type DslTypeSpec = { type: string; fields: Array } diff --git a/fe/src/types/generated/DslValidateRequest.ts b/fe/src/types/generated/DslValidateRequest.ts index a1ea4d48..e33f232f 100644 --- a/fe/src/types/generated/DslValidateRequest.ts +++ b/fe/src/types/generated/DslValidateRequest.ts @@ -1,7 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DslValidateRequest = { -/** - * The DSL steps array (JSON). Example: { "steps": [ { "from": { ... }, "transform": { ... }, "to": { ... } } ] } - */ -steps: unknown[], }; +export type DslValidateRequest = { + /** + * The DSL steps array (JSON). Example: { "steps": [ { "from": { ... }, "transform": { ... }, "to": { ... } } ] } + */ + steps: unknown[] +} diff --git a/fe/src/types/generated/DslValidateResponse.ts b/fe/src/types/generated/DslValidateResponse.ts index 0ab99941..72cf934b 100644 --- a/fe/src/types/generated/DslValidateResponse.ts +++ b/fe/src/types/generated/DslValidateResponse.ts @@ -1,7 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DslValidateResponse = { -/** - * Whether the DSL is valid - */ -valid: boolean, }; +export type DslValidateResponse = { + /** + * Whether the DSL is valid + */ + valid: boolean +} diff --git a/fe/src/types/generated/EmailTemplateListQuery.ts b/fe/src/types/generated/EmailTemplateListQuery.ts index 6a520c45..4779fff2 100644 --- a/fe/src/types/generated/EmailTemplateListQuery.ts +++ b/fe/src/types/generated/EmailTemplateListQuery.ts @@ -1,10 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EmailTemplateType } from './EmailTemplateType' /** * Query parameters for filtering email templates */ -export type EmailTemplateListQuery = { -/** - * Filter by template type: "system" or "workflow" - */ -type: string | null, }; +export type EmailTemplateListQuery = { + /** + * Filter by template type: "system" or "workflow" + */ + type: EmailTemplateType | null +} diff --git a/fe/src/types/generated/EmailTemplateResponse.ts b/fe/src/types/generated/EmailTemplateResponse.ts index 94cffc19..d24719a1 100644 --- a/fe/src/types/generated/EmailTemplateResponse.ts +++ b/fe/src/types/generated/EmailTemplateResponse.ts @@ -3,44 +3,45 @@ /** * Email template response DTO */ -export type EmailTemplateResponse = { -/** - * Template UUID - */ -uuid: string, -/** - * Display name - */ -name: string, -/** - * Unique slug identifier - */ -slug: string, -/** - * Template type (system or workflow) - */ -template_type: string, -/** - * Subject line template - */ -subject_template: string, -/** - * HTML body template - */ -body_html_template: string, -/** - * Plain-text body template - */ -body_text_template: string, -/** - * Available template variables - */ -variables: unknown, -/** - * ISO 8601 creation timestamp - */ -created_at: string, -/** - * ISO 8601 last-updated timestamp - */ -updated_at: string, }; +export type EmailTemplateResponse = { + /** + * Template UUID + */ + uuid: string + /** + * Display name + */ + name: string + /** + * Unique slug identifier + */ + slug: string + /** + * Template type (system or workflow) + */ + template_type: string + /** + * Subject line template + */ + subject_template: string + /** + * HTML body template + */ + body_html_template: string + /** + * Plain-text body template + */ + body_text_template: string + /** + * Available template variables + */ + variables: unknown + /** + * ISO 8601 creation timestamp + */ + created_at: string + /** + * ISO 8601 last-updated timestamp + */ + updated_at: string +} diff --git a/fe/src/types/generated/EmailTemplateType.ts b/fe/src/types/generated/EmailTemplateType.ts index 29904876..3e4cc224 100644 --- a/fe/src/types/generated/EmailTemplateType.ts +++ b/fe/src/types/generated/EmailTemplateType.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type EmailTemplateType = "system" | "workflow"; +export type EmailTemplateType = 'system' | 'workflow' diff --git a/fe/src/types/generated/EntityDefinitionListResponse.ts b/fe/src/types/generated/EntityDefinitionListResponse.ts deleted file mode 100644 index 26123f19..00000000 --- a/fe/src/types/generated/EntityDefinitionListResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { EntityDefinitionSchema } from "./EntityDefinitionSchema"; - -/** - * Response for listing entity definitions - */ -export type EntityDefinitionListResponse = { -/** - * List of entity definitions - */ -items: Array, -/** - * Total number of items - */ -total: number, }; diff --git a/fe/src/types/generated/EntityDefinitionSchema.ts b/fe/src/types/generated/EntityDefinitionSchema.ts index ff760267..4feddbe6 100644 --- a/fe/src/types/generated/EntityDefinitionSchema.ts +++ b/fe/src/types/generated/EntityDefinitionSchema.ts @@ -1,52 +1,53 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FieldDefinitionSchema } from "./FieldDefinitionSchema"; +import type { FieldDefinitionSchema } from './FieldDefinitionSchema' /** * Schema for entity definitions in `OpenAPI` docs * Used to define entity types with their fields and metadata */ -export type EntityDefinitionSchema = { -/** - * Unique identifier (automatically generated if not provided) - */ -uuid: string | null, -/** - * Entity type name (must be unique, alphanumeric with underscores, no spaces) - */ -entity_type: string, -/** - * User-friendly display name for this entity type - */ -display_name: string, -/** - * Description of this entity type - */ -description: string | null, -/** - * Group name for organizing entity types - */ -group_name: string | null, -/** - * Whether this entity type can have children - */ -allow_children: boolean, -/** - * Icon identifier for this entity type - */ -icon: string | null, -/** - * Field definitions for this entity type - */ -fields: Array, -/** - * Published &**state (whether visible to users) - */ -published: boolean | null, -/** - * Created at timestamp - */ -created_at: string | null, -/** - * Updated at timestamp - */ -updated_at: string | null, }; +export type EntityDefinitionSchema = { + /** + * Unique identifier (automatically generated if not provided) + */ + uuid: string | null + /** + * Entity type name (must be unique, alphanumeric with underscores, no spaces) + */ + entity_type: string + /** + * User-friendly display name for this entity type + */ + display_name: string + /** + * Description of this entity type + */ + description: string | null + /** + * Group name for organizing entity types + */ + group_name: string | null + /** + * Whether this entity type can have children + */ + allow_children: boolean + /** + * Icon identifier for this entity type + */ + icon: string | null + /** + * Field definitions for this entity type + */ + fields: Array + /** + * Published &**state (whether visible to users) + */ + published: boolean | null + /** + * Created at timestamp + */ + created_at: string | null + /** + * Updated at timestamp + */ + updated_at: string | null +} diff --git a/fe/src/types/generated/EntityDefinitionVersionMeta.ts b/fe/src/types/generated/EntityDefinitionVersionMeta.ts index 80c0e2c1..660e0a40 100644 --- a/fe/src/types/generated/EntityDefinitionVersionMeta.ts +++ b/fe/src/types/generated/EntityDefinitionVersionMeta.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type EntityDefinitionVersionMeta = { version_number: number, created_at: string, created_by: string | null, created_by_name: string | null, }; +export type EntityDefinitionVersionMeta = { + version_number: number + created_at: string + created_by: string | null + created_by_name: string | null +} diff --git a/fe/src/types/generated/EntityDefinitionVersionPayload.ts b/fe/src/types/generated/EntityDefinitionVersionPayload.ts index 06b6806e..190940f7 100644 --- a/fe/src/types/generated/EntityDefinitionVersionPayload.ts +++ b/fe/src/types/generated/EntityDefinitionVersionPayload.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type EntityDefinitionVersionPayload = { version_number: number, created_at: string, created_by: string | null, data: unknown, }; +export type EntityDefinitionVersionPayload = { + version_number: number + created_at: string + created_by: string | null + data: unknown +} diff --git a/fe/src/types/generated/EntityFieldInfo.ts b/fe/src/types/generated/EntityFieldInfo.ts new file mode 100644 index 00000000..3210e0dd --- /dev/null +++ b/fe/src/types/generated/EntityFieldInfo.ts @@ -0,0 +1,23 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Per-field metadata returned by `GET /entity-definitions/{type}/fields` + */ +export type EntityFieldInfo = { + /** + * Field column name + */ + name: string + /** + * Field type (as declared in the entity definition) + */ + type: string + /** + * Whether the field is required + */ + required: boolean + /** + * Whether the field is a BE-managed system field (not user-definable) + */ + system: boolean +} diff --git a/fe/src/types/generated/EntityStats.ts b/fe/src/types/generated/EntityStats.ts new file mode 100644 index 00000000..362b3fbf --- /dev/null +++ b/fe/src/types/generated/EntityStats.ts @@ -0,0 +1,16 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EntityTypeCount } from './EntityTypeCount' + +/** + * Entity statistics + */ +export type EntityStats = { + /** + * Total count of all entities across all types + */ + total: number + /** + * Breakdown by entity type + */ + by_type: Array +} diff --git a/fe/src/types/generated/EntityTypeCount.ts b/fe/src/types/generated/EntityTypeCount.ts new file mode 100644 index 00000000..ef4b37c2 --- /dev/null +++ b/fe/src/types/generated/EntityTypeCount.ts @@ -0,0 +1,15 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Entity count for a specific type + */ +export type EntityTypeCount = { + /** + * Entity type name + */ + entity_type: string + /** + * Count of entities of this type + */ + count: number +} diff --git a/fe/src/types/generated/EntityVersioningSettingsDto.ts b/fe/src/types/generated/EntityVersioningSettingsDto.ts new file mode 100644 index 00000000..3460cd47 --- /dev/null +++ b/fe/src/types/generated/EntityVersioningSettingsDto.ts @@ -0,0 +1,22 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * DTO for entity versioning settings (API layer wrapper) + * + * This is a thin wrapper around the core `EntityVersioningSettings` type + * to add `OpenAPI` schema generation support. + */ +export type EntityVersioningSettingsDto = { + /** + * Whether entity versioning is enabled + */ + enabled: boolean + /** + * Maximum number of versions to keep per entity + */ + max_versions: number | null + /** + * Maximum age in days for versions + */ + max_age_days: number | null +} diff --git a/fe/src/types/generated/FieldConstraints.ts b/fe/src/types/generated/FieldConstraints.ts index c22c0f1e..9292ce38 100644 --- a/fe/src/types/generated/FieldConstraints.ts +++ b/fe/src/types/generated/FieldConstraints.ts @@ -1,12 +1,22 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DateTimeConstraints } from "./DateTimeConstraints"; -import type { NumericConstraints } from "./NumericConstraints"; -import type { RelationConstraints } from "./RelationConstraints"; -import type { SchemaConstraints } from "./SchemaConstraints"; -import type { SelectConstraints } from "./SelectConstraints"; -import type { StringConstraints } from "./StringConstraints"; +import type { DateTimeConstraints } from './DateTimeConstraints' +import type { NumericConstraints } from './NumericConstraints' +import type { RelationConstraints } from './RelationConstraints' +import type { SchemaConstraints } from './SchemaConstraints' +import type { SelectConstraints } from './SelectConstraints' +import type { StringConstraints } from './StringConstraints' /** * Field constraints based on field type */ -export type FieldConstraints = { "type": "string", "constraints": StringConstraints } | { "type": "integer", "constraints": NumericConstraints } | { "type": "float", "constraints": NumericConstraints } | { "type": "datetime", "constraints": DateTimeConstraints } | { "type": "date", "constraints": DateTimeConstraints } | { "type": "select", "constraints": SelectConstraints } | { "type": "multiselect", "constraints": SelectConstraints } | { "type": "relation", "constraints": RelationConstraints } | { "type": "schema", "constraints": SchemaConstraints } | { "type": "none" }; +export type FieldConstraints = + | { type: 'string'; constraints: StringConstraints } + | { type: 'integer'; constraints: NumericConstraints } + | { type: 'float'; constraints: NumericConstraints } + | { type: 'datetime'; constraints: DateTimeConstraints } + | { type: 'date'; constraints: DateTimeConstraints } + | { type: 'select'; constraints: SelectConstraints } + | { type: 'multiselect'; constraints: SelectConstraints } + | { type: 'relation'; constraints: RelationConstraints } + | { type: 'schema'; constraints: SchemaConstraints } + | { type: 'none' } diff --git a/fe/src/types/generated/FieldDefinitionSchema.ts b/fe/src/types/generated/FieldDefinitionSchema.ts index 039d6abe..11239829 100644 --- a/fe/src/types/generated/FieldDefinitionSchema.ts +++ b/fe/src/types/generated/FieldDefinitionSchema.ts @@ -1,53 +1,54 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FieldConstraints } from "./FieldConstraints"; -import type { FieldTypeSchema } from "./FieldTypeSchema"; -import type { UiSettingsSchema } from "./UiSettingsSchema"; +import type { FieldConstraints } from './FieldConstraints' +import type { FieldTypeSchema } from './FieldTypeSchema' +import type { UiSettingsSchema } from './UiSettingsSchema' /** * Schema for field definitions in `OpenAPI` docs */ -export type FieldDefinitionSchema = { -/** - * Field name (must be unique within class and contain only alphanumeric characters, underscores, no spaces) - */ -name: string, -/** - * User-friendly display name - */ -display_name: string, -/** - * Field data type - */ -field_type: FieldTypeSchema, -/** - * Field description - */ -description: string | null, -/** - * Whether the field is required - */ -required: boolean, -/** - * Whether the field is indexed for faster searches - */ -indexed: boolean, -/** - * Whether the field can be used in API filtering - */ -filterable: boolean, -/** - * Whether the field must have unique values (DB-level constraint) - */ -unique: boolean, -/** - * Default value for the field - */ -default_value: unknown, -/** - * Type-specific field constraints - */ -constraints: FieldConstraints | null, -/** - * UI settings for the field - */ -ui_settings: UiSettingsSchema, }; +export type FieldDefinitionSchema = { + /** + * Field name (must be unique within class and contain only alphanumeric characters, underscores, no spaces) + */ + name: string + /** + * User-friendly display name + */ + display_name: string + /** + * Field data type + */ + field_type: FieldTypeSchema + /** + * Field description + */ + description: string | null + /** + * Whether the field is required + */ + required: boolean + /** + * Whether the field is indexed for faster searches + */ + indexed: boolean + /** + * Whether the field can be used in API filtering + */ + filterable: boolean + /** + * Whether the field must have unique values (DB-level constraint) + */ + unique: boolean + /** + * Default value for the field + */ + default_value: unknown + /** + * Type-specific field constraints + */ + constraints: FieldConstraints | null + /** + * UI settings for the field + */ + ui_settings: UiSettingsSchema +} diff --git a/fe/src/types/generated/FieldTypeSchema.ts b/fe/src/types/generated/FieldTypeSchema.ts index 13828671..5a8a4094 100644 --- a/fe/src/types/generated/FieldTypeSchema.ts +++ b/fe/src/types/generated/FieldTypeSchema.ts @@ -3,4 +3,23 @@ /** * Field types available for entity definitions */ -export type FieldTypeSchema = "String" | "Text" | "Wysiwyg" | "Integer" | "Float" | "Boolean" | "DateTime" | "Date" | "Object" | "Array" | "Json" | "Uuid" | "ManyToOne" | "ManyToMany" | "Select" | "MultiSelect" | "Image" | "File" | "Password"; +export type FieldTypeSchema = + | 'String' + | 'Text' + | 'Wysiwyg' + | 'Integer' + | 'Float' + | 'Boolean' + | 'DateTime' + | 'Date' + | 'Object' + | 'Array' + | 'Json' + | 'Uuid' + | 'ManyToOne' + | 'ManyToMany' + | 'Select' + | 'MultiSelect' + | 'Image' + | 'File' + | 'Password' diff --git a/fe/src/types/generated/HealthData.ts b/fe/src/types/generated/HealthData.ts index d46e4dde..43a7e74a 100644 --- a/fe/src/types/generated/HealthData.ts +++ b/fe/src/types/generated/HealthData.ts @@ -3,20 +3,21 @@ /** * Health check response data */ -export type HealthData = { -/** - * Current date and time - */ -date: string, -/** - * Generated UUID for this health check - */ -uuid: string, -/** - * Route that was accessed - */ -route: string, -/** - * User agent that made the request - */ -agent: string, }; +export type HealthData = { + /** + * Current date and time + */ + date: string + /** + * Generated UUID for this health check + */ + uuid: string + /** + * Route that was accessed + */ + route: string + /** + * User agent that made the request + */ + agent: string +} diff --git a/fe/src/types/generated/LicenseStateDto.ts b/fe/src/types/generated/LicenseStateDto.ts new file mode 100644 index 00000000..48d0c9dc --- /dev/null +++ b/fe/src/types/generated/LicenseStateDto.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * License state enumeration + */ +export type LicenseStateDto = 'none' | 'invalid' | 'error' | 'valid' diff --git a/fe/src/types/generated/LicenseStatusDto.ts b/fe/src/types/generated/LicenseStatusDto.ts new file mode 100644 index 00000000..3d6025e5 --- /dev/null +++ b/fe/src/types/generated/LicenseStatusDto.ts @@ -0,0 +1,44 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LicenseStateDto } from './LicenseStateDto' + +/** + * DTO for license status + */ +export type LicenseStatusDto = { + /** + * License state + */ + state: LicenseStateDto + /** + * Company name (if license is present) + */ + company: string | null + /** + * License type (if license is present) + */ + license_type: string | null + /** + * License ID (if license is present) + */ + license_id: string | null + /** + * Issue date (if license is present) + */ + issued_at: string | null + /** + * Expiration date (if license is present and has expiration) + */ + expires_at: string | null + /** + * License version + */ + version: string | null + /** + * Verification timestamp + */ + verified_at: string + /** + * Error message (only present if state is "error" or "invalid") + */ + error_message: string | null +} diff --git a/fe/src/types/generated/LogoutRequest.ts b/fe/src/types/generated/LogoutRequest.ts index 56fd7d3d..8a68e720 100644 --- a/fe/src/types/generated/LogoutRequest.ts +++ b/fe/src/types/generated/LogoutRequest.ts @@ -3,8 +3,9 @@ /** * Request to logout with refresh token */ -export type LogoutRequest = { -/** - * Refresh token to revoke - */ -refresh_token: string, }; +export type LogoutRequest = { + /** + * Refresh token to revoke + */ + refresh_token: string +} diff --git a/fe/src/types/generated/NumericConstraints.ts b/fe/src/types/generated/NumericConstraints.ts index c8c4ab3b..395d72f3 100644 --- a/fe/src/types/generated/NumericConstraints.ts +++ b/fe/src/types/generated/NumericConstraints.ts @@ -3,20 +3,21 @@ /** * Numeric field constraints */ -export type NumericConstraints = { -/** - * Minimum allowed value - */ -min: number | null, -/** - * Maximum allowed value - */ -max: number | null, -/** - * Decimal precision for float values - */ -precision: number | null, -/** - * Whether only positive values are allowed - */ -positive_only: boolean | null, }; +export type NumericConstraints = { + /** + * Minimum allowed value + */ + min: number | null + /** + * Maximum allowed value + */ + max: number | null + /** + * Decimal precision for float values + */ + precision: number | null + /** + * Whether only positive values are allowed + */ + positive_only: boolean | null +} diff --git a/fe/src/types/generated/OnComplete.ts b/fe/src/types/generated/OnComplete.ts index b4e28b5b..48fe8b6f 100644 --- a/fe/src/types/generated/OnComplete.ts +++ b/fe/src/types/generated/OnComplete.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PostRunAction } from "./PostRunAction"; +import type { PostRunAction } from './PostRunAction' /** * Actions to execute after all items in a workflow run have been processed. */ -export type OnComplete = { actions: Array, }; +export type OnComplete = { actions: Array } diff --git a/fe/src/types/generated/OptionsSourceSchema.ts b/fe/src/types/generated/OptionsSourceSchema.ts deleted file mode 100644 index 24867a30..00000000 --- a/fe/src/types/generated/OptionsSourceSchema.ts +++ /dev/null @@ -1,24 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SelectOptionSchema } from "./SelectOptionSchema"; - -/** - * Schema for options source in `OpenAPI` docs - * Defines how to populate options for `Select` and `MultiSelect` fields - */ -export type OptionsSourceSchema = { "type": "fixed", options: Array, } | { "type": "enum", enum_name: string, } | { "type": "query", -/** - * Target entity type to query - */ -entity_type: string, -/** - * Field to use as option value - */ -value_field: string, -/** - * Field to use as option display label - */ -label_field: string, -/** - * Optional filter criteria for the query - */ -filter: unknown, }; diff --git a/fe/src/types/generated/PaginationMeta.ts b/fe/src/types/generated/PaginationMeta.ts index 8b974506..c335f901 100644 --- a/fe/src/types/generated/PaginationMeta.ts +++ b/fe/src/types/generated/PaginationMeta.ts @@ -3,28 +3,29 @@ /** * Metadata for paginated responses */ -export type PaginationMeta = { -/** - * Total number of items available - */ -total: number, -/** - * Current page number - */ -page: number, -/** - * Items per page - */ -per_page: number, -/** - * Total number of pages - */ -total_pages: number, -/** - * If there is a previous page - */ -has_previous: boolean, -/** - * If there is a next page - */ -has_next: boolean, }; +export type PaginationMeta = { + /** + * Total number of items available + */ + total: number + /** + * Current page number + */ + page: number + /** + * Items per page + */ + per_page: number + /** + * Total number of pages + */ + total_pages: number + /** + * If there is a previous page + */ + has_previous: boolean + /** + * If there is a next page + */ + has_next: boolean +} diff --git a/fe/src/types/generated/PaginationQuery.ts b/fe/src/types/generated/PaginationQuery.ts new file mode 100644 index 00000000..ca1e32e9 --- /dev/null +++ b/fe/src/types/generated/PaginationQuery.ts @@ -0,0 +1,40 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Flexible pagination query parameters that support both `page`/`per_page` and `limit`/`offset` + * + * This struct provides a unified pagination interface that supports two common pagination patterns: + * + * 1. **Page-based pagination**: Use `page` and `per_page` parameters + * - `page`: Page number (1-based, default: 1) + * - `per_page`: Number of items per page (default: 20, max: 100) + * + * 2. **Offset-based pagination**: Use `limit` and `offset` parameters + * - `limit`: Maximum number of items to return (default: 20, max: 100) + * - `offset`: Number of items to skip (default: 0) + * + * All parameters are optional and have sensible defaults. You can mix and match these parameters + * as needed for your use case. + */ +export type PaginationQuery = { + /** + * Page number (1-based) - defaults to 1 + * Use with `per_page` for page-based pagination + */ + page: number | null + /** + * Items per page - defaults to 20, max 100 + * Use with `page` for page-based pagination + */ + per_page: number | null + /** + * Limit (alternative to `per_page`) - defaults to 20, max 100 + * Use with `offset` for offset-based pagination + */ + limit: number | null + /** + * Offset (alternative to page) - defaults to 0 + * Use with `limit` for offset-based pagination + */ + offset: number | null +} diff --git a/fe/src/types/generated/PermissionResponse.ts b/fe/src/types/generated/PermissionResponse.ts index c98c1da0..a58706e9 100644 --- a/fe/src/types/generated/PermissionResponse.ts +++ b/fe/src/types/generated/PermissionResponse.ts @@ -1,28 +1,29 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AccessLevel } from "./AccessLevel"; -import type { PermissionType } from "./PermissionType"; +import type { AccessLevel } from './AccessLevel' +import type { PermissionType } from './PermissionType' /** * Permission response DTO (for API serialization) */ -export type PermissionResponse = { -/** - * Resource type (as string for API compatibility) - */ -resource_type: string, -/** - * Permission type - */ -permission_type: PermissionType, -/** - * Access level - */ -access_level: AccessLevel, -/** - * Resource UUIDs this permission applies to - */ -resource_uuids: string[], -/** - * Additional constraints - */ -constraints: unknown, }; +export type PermissionResponse = { + /** + * Resource type (as string for API compatibility) + */ + resource_type: string + /** + * Permission type + */ + permission_type: PermissionType + /** + * Access level + */ + access_level: AccessLevel + /** + * Resource UUIDs this permission applies to + */ + resource_uuids: string[] + /** + * Additional constraints + */ + constraints: unknown +} diff --git a/fe/src/types/generated/PermissionType.ts b/fe/src/types/generated/PermissionType.ts index 68d8130f..7fedb81a 100644 --- a/fe/src/types/generated/PermissionType.ts +++ b/fe/src/types/generated/PermissionType.ts @@ -3,4 +3,11 @@ /** * Permission types that can be granted */ -export type PermissionType = "Read" | "Create" | "Update" | "Delete" | "Publish" | "Admin" | "Execute"; +export type PermissionType = + | 'Read' + | 'Create' + | 'Update' + | 'Delete' + | 'Publish' + | 'Admin' + | 'Execute' diff --git a/fe/src/types/generated/PostRunAction.ts b/fe/src/types/generated/PostRunAction.ts index 19493117..61bfd8a6 100644 --- a/fe/src/types/generated/PostRunAction.ts +++ b/fe/src/types/generated/PostRunAction.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PostRunSendEmail } from "./PostRunSendEmail"; +import type { PostRunSendEmail } from './PostRunSendEmail' /** * A single post-run action. */ -export type PostRunAction = { "type": "send_email" } & PostRunSendEmail; +export type PostRunAction = { type: 'send_email' } & PostRunSendEmail diff --git a/fe/src/types/generated/PostRunCondition.ts b/fe/src/types/generated/PostRunCondition.ts index 05b9b0fd..efe3276b 100644 --- a/fe/src/types/generated/PostRunCondition.ts +++ b/fe/src/types/generated/PostRunCondition.ts @@ -3,4 +3,4 @@ /** * Condition for when a post-run action fires. */ -export type PostRunCondition = "always" | "on_success" | "on_failure"; +export type PostRunCondition = 'always' | 'on_success' | 'on_failure' diff --git a/fe/src/types/generated/PostRunSendEmail.ts b/fe/src/types/generated/PostRunSendEmail.ts index 0568890d..d86a1c27 100644 --- a/fe/src/types/generated/PostRunSendEmail.ts +++ b/fe/src/types/generated/PostRunSendEmail.ts @@ -1,24 +1,25 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PostRunCondition } from "./PostRunCondition"; -import type { StringOperand } from "./StringOperand"; +import type { PostRunCondition } from './PostRunCondition' +import type { StringOperand } from './StringOperand' /** * Send an email after the run completes. */ -export type PostRunSendEmail = { -/** - * UUID of a workflow email template - */ -template_uuid: string, -/** - * Recipients (only `const_string` — no field refs in post-run context) - */ -to: Array, -/** - * Optional CC - */ -cc: Array | null, -/** - * When to fire this action - */ -condition: PostRunCondition, }; +export type PostRunSendEmail = { + /** + * UUID of a workflow email template + */ + template_uuid: string + /** + * Recipients (only `const_string` — no field refs in post-run context) + */ + to: Array + /** + * Optional CC + */ + cc: Array | null + /** + * When to fire this action + */ + condition: PostRunCondition +} diff --git a/fe/src/types/generated/ReassignApiKeyRequest.ts b/fe/src/types/generated/ReassignApiKeyRequest.ts index 81effd9f..4b0c5bf0 100644 --- a/fe/src/types/generated/ReassignApiKeyRequest.ts +++ b/fe/src/types/generated/ReassignApiKeyRequest.ts @@ -3,8 +3,9 @@ /** * Request to reassign an API key to a different user */ -export type ReassignApiKeyRequest = { -/** - * UUID of the user to reassign the API key to - */ -user_uuid: string, }; +export type ReassignApiKeyRequest = { + /** + * UUID of the user to reassign the API key to + */ + user_uuid: string +} diff --git a/fe/src/types/generated/RefreshTokenRequest.ts b/fe/src/types/generated/RefreshTokenRequest.ts index bfa70cc0..5e90d4d1 100644 --- a/fe/src/types/generated/RefreshTokenRequest.ts +++ b/fe/src/types/generated/RefreshTokenRequest.ts @@ -3,8 +3,9 @@ /** * Refresh token request body */ -export type RefreshTokenRequest = { -/** - * Refresh token - */ -refresh_token: string, }; +export type RefreshTokenRequest = { + /** + * Refresh token + */ + refresh_token: string +} diff --git a/fe/src/types/generated/RefreshTokenResponse.ts b/fe/src/types/generated/RefreshTokenResponse.ts index 039e4d19..ce0ec91e 100644 --- a/fe/src/types/generated/RefreshTokenResponse.ts +++ b/fe/src/types/generated/RefreshTokenResponse.ts @@ -3,20 +3,21 @@ /** * Refresh token response body */ -export type RefreshTokenResponse = { -/** - * New access token - */ -access_token: string, -/** - * New refresh token - */ -refresh_token: string, -/** - * Access token expiration (RFC3339 timestamp) - */ -access_expires_at: string, -/** - * Refresh token expiration (RFC3339 timestamp) - */ -refresh_expires_at: string, }; +export type RefreshTokenResponse = { + /** + * New access token + */ + access_token: string + /** + * New refresh token + */ + refresh_token: string + /** + * Access token expiration (RFC3339 timestamp) + */ + access_expires_at: string + /** + * Refresh token expiration (RFC3339 timestamp) + */ + refresh_expires_at: string +} diff --git a/fe/src/types/generated/RelationConstraints.ts b/fe/src/types/generated/RelationConstraints.ts index f0c04d4c..2234b600 100644 --- a/fe/src/types/generated/RelationConstraints.ts +++ b/fe/src/types/generated/RelationConstraints.ts @@ -3,8 +3,9 @@ /** * Relation field constraints */ -export type RelationConstraints = { -/** - * Name of the related entity type - */ -target_class: string, }; +export type RelationConstraints = { + /** + * Name of the related entity type + */ + target_class: string +} diff --git a/fe/src/types/generated/ResourceNamespace.ts b/fe/src/types/generated/ResourceNamespace.ts new file mode 100644 index 00000000..3cf22158 --- /dev/null +++ b/fe/src/types/generated/ResourceNamespace.ts @@ -0,0 +1,16 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Resource namespace for permissions + * + * Each namespace represents a different resource type that can have permissions. + */ +export type ResourceNamespace = + | 'Workflows' + | 'Entities' + | 'EntityDefinitions' + | 'ApiKeys' + | 'Roles' + | 'Users' + | 'System' + | 'DashboardStats' diff --git a/fe/src/types/generated/ResponseMeta.ts b/fe/src/types/generated/ResponseMeta.ts index 621aa9ec..e9bb8657 100644 --- a/fe/src/types/generated/ResponseMeta.ts +++ b/fe/src/types/generated/ResponseMeta.ts @@ -1,23 +1,24 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PaginationMeta } from "./PaginationMeta"; +import type { PaginationMeta } from './PaginationMeta' /** * Metadata for API responses */ -export type ResponseMeta = { -/** - * Pagination information (if applicable) - */ -pagination: PaginationMeta | null, -/** - * Request UUID for tracking - */ -request_id: string | null, -/** - * Timestamp of the response - */ -timestamp: string | null, -/** - * Additional custom metadata - */ -custom: unknown, }; +export type ResponseMeta = { + /** + * Pagination information (if applicable) + */ + pagination: PaginationMeta | null + /** + * Request UUID for tracking + */ + request_id: string | null + /** + * Timestamp of the response + */ + timestamp: string | null + /** + * Additional custom metadata + */ + custom: unknown +} diff --git a/fe/src/types/generated/RoleResponse.ts b/fe/src/types/generated/RoleResponse.ts index a6d2cd60..366c3186 100644 --- a/fe/src/types/generated/RoleResponse.ts +++ b/fe/src/types/generated/RoleResponse.ts @@ -1,55 +1,56 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionResponse } from "./PermissionResponse"; +import type { PermissionResponse } from './PermissionResponse' /** * Role response DTO */ -export type RoleResponse = { -/** - * UUID of the role - */ -uuid: string, -/** - * Name of the role - */ -name: string, -/** - * Description of the role - */ -description: string | null, -/** - * Whether this is a system role - */ -is_system: boolean, -/** - * Whether this role grants super admin privileges - */ -super_admin: boolean, -/** - * Direct permissions for this role - */ -permissions: Array, -/** - * When the role was created - */ -created_at: string, -/** - * When the role was last updated - */ -updated_at: string, -/** - * UUID of the user who created the role - */ -created_by: string, -/** - * UUID of the user who last updated the role - */ -updated_by: string | null, -/** - * Whether the role is published - */ -published: boolean, -/** - * Version number - */ -version: number, }; +export type RoleResponse = { + /** + * UUID of the role + */ + uuid: string + /** + * Name of the role + */ + name: string + /** + * Description of the role + */ + description: string | null + /** + * Whether this is a system role + */ + is_system: boolean + /** + * Whether this role grants super admin privileges + */ + super_admin: boolean + /** + * Direct permissions for this role + */ + permissions: Array + /** + * When the role was created + */ + created_at: string + /** + * When the role was last updated + */ + updated_at: string + /** + * UUID of the user who created the role + */ + created_by: string + /** + * UUID of the user who last updated the role + */ + updated_by: string | null + /** + * Whether the role is published + */ + published: boolean + /** + * Version number + */ + version: number +} diff --git a/fe/src/types/generated/SchemaConstraints.ts b/fe/src/types/generated/SchemaConstraints.ts index d7f8e626..d0b33cee 100644 --- a/fe/src/types/generated/SchemaConstraints.ts +++ b/fe/src/types/generated/SchemaConstraints.ts @@ -3,8 +3,9 @@ /** * Object/Array field constraints */ -export type SchemaConstraints = { -/** - * JSON schema for validating the object/array structure - */ -schema: unknown, }; +export type SchemaConstraints = { + /** + * JSON schema for validating the object/array structure + */ + schema: unknown +} diff --git a/fe/src/types/generated/SelectConstraints.ts b/fe/src/types/generated/SelectConstraints.ts index bbd33dec..b6de38b2 100644 --- a/fe/src/types/generated/SelectConstraints.ts +++ b/fe/src/types/generated/SelectConstraints.ts @@ -3,8 +3,9 @@ /** * Select field constraints */ -export type SelectConstraints = { -/** - * Array of allowed values - */ -options: Array | null, }; +export type SelectConstraints = { + /** + * Array of allowed values + */ + options: Array | null +} diff --git a/fe/src/types/generated/SelectOptionSchema.ts b/fe/src/types/generated/SelectOptionSchema.ts deleted file mode 100644 index 93ff46c9..00000000 --- a/fe/src/types/generated/SelectOptionSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Schema for select options in `OpenAPI` docs - * Used for defining individual options in fixed option lists - */ -export type SelectOptionSchema = { -/** - * Option value (stored in database) - */ -value: string, -/** - * Option display label (shown in UI) - */ -label: string, }; diff --git a/fe/src/types/generated/SendEmailTransform.ts b/fe/src/types/generated/SendEmailTransform.ts index 1d83a99f..9d639d7d 100644 --- a/fe/src/types/generated/SendEmailTransform.ts +++ b/fe/src/types/generated/SendEmailTransform.ts @@ -1,23 +1,24 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { StringOperand } from "./StringOperand"; +import type { StringOperand } from './StringOperand' /** * Send an email via SMTP using a workflow email template */ -export type SendEmailTransform = { -/** - * UUID of a workflow email template - */ -template_uuid: string, -/** - * Recipients: field refs or constant email addresses - */ -to: Array, -/** - * Optional CC recipients - */ -cc: Array | null, -/** - * Normalized field to store send result (`"queued"`, `"mail_not_configured"`, or error) - */ -target_status: string, }; +export type SendEmailTransform = { + /** + * UUID of a workflow email template + */ + template_uuid: string + /** + * Recipients: field refs or constant email addresses + */ + to: Array + /** + * Optional CC recipients + */ + cc: Array | null + /** + * Normalized field to store send result (`"queued"`, `"mail_not_configured"`, or error) + */ + target_status: string +} diff --git a/fe/src/types/generated/SortingQuery.ts b/fe/src/types/generated/SortingQuery.ts new file mode 100644 index 00000000..1ef1002f --- /dev/null +++ b/fe/src/types/generated/SortingQuery.ts @@ -0,0 +1,15 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Standard sorting query parameters + */ +export type SortingQuery = { + /** + * Field to sort by + */ + sort_by: string | null + /** + * Sort order (asc or desc) + */ + sort_order: string | null +} diff --git a/fe/src/types/generated/Status.ts b/fe/src/types/generated/Status.ts index de7249f2..5d3cd8e4 100644 --- a/fe/src/types/generated/Status.ts +++ b/fe/src/types/generated/Status.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Status = "Success" | "Error"; +export type Status = 'Success' | 'Error' diff --git a/fe/src/types/generated/StringConstraints.ts b/fe/src/types/generated/StringConstraints.ts index 11526604..ca7185c6 100644 --- a/fe/src/types/generated/StringConstraints.ts +++ b/fe/src/types/generated/StringConstraints.ts @@ -3,20 +3,21 @@ /** * String field constraints */ -export type StringConstraints = { -/** - * Minimum string length - */ -min_length: number | null, -/** - * Maximum string length - */ -max_length: number | null, -/** - * Regex pattern for validation (e.g., "^[A-Z0-9]{2,20}$") - */ -pattern: string | null, -/** - * Custom error message when validation fails - */ -error_message: string | null, }; +export type StringConstraints = { + /** + * Minimum string length + */ + min_length: number | null + /** + * Maximum string length + */ + max_length: number | null + /** + * Regex pattern for validation (e.g., "^[A-Z0-9]{2,20}$") + */ + pattern: string | null + /** + * Custom error message when validation fails + */ + error_message: string | null +} diff --git a/fe/src/types/generated/StringOperand.ts b/fe/src/types/generated/StringOperand.ts index 9714738f..f016a56a 100644 --- a/fe/src/types/generated/StringOperand.ts +++ b/fe/src/types/generated/StringOperand.ts @@ -3,4 +3,6 @@ /** * String operand variant used by Concat transform */ -export type StringOperand = { "kind": "field", field: string, } | { "kind": "const_string", value: string, }; +export type StringOperand = + | { kind: 'field'; field: string } + | { kind: 'const_string'; value: string } diff --git a/fe/src/types/generated/SystemLogDto.ts b/fe/src/types/generated/SystemLogDto.ts index 217735a5..ea2b86ff 100644 --- a/fe/src/types/generated/SystemLogDto.ts +++ b/fe/src/types/generated/SystemLogDto.ts @@ -3,40 +3,41 @@ /** * Single system log entry response */ -export type SystemLogDto = { -/** - * Log entry UUID - */ -uuid: string, -/** - * When this log entry was created - */ -created_at: string, -/** - * UUID of the user that triggered the event (if known) - */ -created_by: string | null, -/** - * Status of the logged event - */ -status: string, -/** - * Type of log entry - */ -log_type: string, -/** - * Type of resource this log entry relates to - */ -resource_type: string, -/** - * UUID of the affected resource (if applicable) - */ -resource_uuid: string | null, -/** - * Short human-readable summary - */ -summary: string, -/** - * Optional structured details (JSONB) - */ -details: unknown, }; +export type SystemLogDto = { + /** + * Log entry UUID + */ + uuid: string + /** + * When this log entry was created + */ + created_at: string + /** + * UUID of the user that triggered the event (if known) + */ + created_by: string | null + /** + * Status of the logged event + */ + status: string + /** + * Type of log entry + */ + log_type: string + /** + * Type of resource this log entry relates to + */ + resource_type: string + /** + * UUID of the affected resource (if applicable) + */ + resource_uuid: string | null + /** + * Short human-readable summary + */ + summary: string + /** + * Optional structured details (JSONB) + */ + details: unknown +} diff --git a/fe/src/types/generated/SystemLogQuery.ts b/fe/src/types/generated/SystemLogQuery.ts index e6b7465c..e0da329f 100644 --- a/fe/src/types/generated/SystemLogQuery.ts +++ b/fe/src/types/generated/SystemLogQuery.ts @@ -1,38 +1,42 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SystemLogResourceType } from './SystemLogResourceType' +import type { SystemLogStatus } from './SystemLogStatus' +import type { SystemLogType } from './SystemLogType' /** * Query parameters for filtering system logs */ -export type SystemLogQuery = { -/** - * Page number (1-based, default: 1) - */ -page: bigint | null, -/** - * Items per page (default: 20, max: 100) - */ -page_size: bigint | null, -/** - * Filter by log type - */ -log_type: string | null, -/** - * Filter by resource type - */ -resource_type: string | null, -/** - * Filter by status - */ -status: string | null, -/** - * Filter by resource UUID - */ -resource_uuid: string | null, -/** - * Filter logs created after this timestamp (ISO 8601) - */ -date_from: string | null, -/** - * Filter logs created before this timestamp (ISO 8601) - */ -date_to: string | null, }; +export type SystemLogQuery = { + /** + * Page number (1-based, default: 1) + */ + page: number | null + /** + * Items per page (default: 20, max: 100) + */ + page_size: number | null + /** + * Filter by log type + */ + log_type: SystemLogType | null + /** + * Filter by resource type + */ + resource_type: SystemLogResourceType | null + /** + * Filter by status + */ + status: SystemLogStatus | null + /** + * Filter by resource UUID + */ + resource_uuid: string | null + /** + * Filter logs created after this timestamp (ISO 8601) + */ + date_from: string | null + /** + * Filter logs created before this timestamp (ISO 8601) + */ + date_to: string | null +} diff --git a/fe/src/types/generated/SystemLogResourceType.ts b/fe/src/types/generated/SystemLogResourceType.ts index d34ddf7a..ecb1a55a 100644 --- a/fe/src/types/generated/SystemLogResourceType.ts +++ b/fe/src/types/generated/SystemLogResourceType.ts @@ -1,3 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SystemLogResourceType = "email" | "admin_user" | "role" | "workflow" | "entity_definition" | "email_template" | "api_key" | "system_settings"; +export type SystemLogResourceType = + | 'email' + | 'admin_user' + | 'role' + | 'workflow' + | 'entity_definition' + | 'email_template' + | 'api_key' + | 'system_settings' diff --git a/fe/src/types/generated/SystemLogStatus.ts b/fe/src/types/generated/SystemLogStatus.ts index 78cb55e5..8b472469 100644 --- a/fe/src/types/generated/SystemLogStatus.ts +++ b/fe/src/types/generated/SystemLogStatus.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SystemLogStatus = "success" | "failed" | "pending"; +export type SystemLogStatus = 'success' | 'failed' | 'pending' diff --git a/fe/src/types/generated/SystemLogType.ts b/fe/src/types/generated/SystemLogType.ts index 26a0c9e3..a5550422 100644 --- a/fe/src/types/generated/SystemLogType.ts +++ b/fe/src/types/generated/SystemLogType.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SystemLogType = "email_sent" | "entity_created" | "entity_updated" | "entity_deleted" | "auth_event"; +export type SystemLogType = + | 'email_sent' + | 'entity_created' + | 'entity_updated' + | 'entity_deleted' + | 'auth_event' diff --git a/fe/src/types/generated/SystemVersionsDto.ts b/fe/src/types/generated/SystemVersionsDto.ts new file mode 100644 index 00000000..29aa17ed --- /dev/null +++ b/fe/src/types/generated/SystemVersionsDto.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ComponentVersionDto } from './ComponentVersionDto' + +/** + * System versions response + */ +export type SystemVersionsDto = { + /** + * Core/API server version + */ + core: string + /** + * Worker component version (if available) + */ + worker: ComponentVersionDto | null + /** + * Maintenance component version (if available) + */ + maintenance: ComponentVersionDto | null +} diff --git a/fe/src/types/generated/UiSettingsSchema.ts b/fe/src/types/generated/UiSettingsSchema.ts index 08c88dc7..bbaf36ec 100644 --- a/fe/src/types/generated/UiSettingsSchema.ts +++ b/fe/src/types/generated/UiSettingsSchema.ts @@ -4,40 +4,41 @@ * Schema for UI settings in `OpenAPI` docs * Controls how fields are rendered in forms and lists */ -export type UiSettingsSchema = { -/** - * Placeholder text shown in empty input fields - */ -placeholder: string | null, -/** - * Help text shown below the field to provide additional context - */ -help_text: string | null, -/** - * Whether to hide this field in list views - */ -hide_in_lists: boolean | null, -/** - * Layout width in grid units (1-12, where 12 is full width) - */ -width: number | null, -/** - * Field display order in forms (lower numbers appear first) - */ -order: number | null, -/** - * Group name for organizing fields into sections - */ -group: string | null, -/** - * Custom CSS class to apply to the field container - */ -css_class: string | null, -/** - * Configuration for WYSIWYG editor toolbar (for Wysiwyg fields) - */ -wysiwyg_toolbar: string | null, -/** - * HTML input type attribute (e.g., "password", "email", "tel") - */ -input_type: string | null, }; +export type UiSettingsSchema = { + /** + * Placeholder text shown in empty input fields + */ + placeholder: string | null + /** + * Help text shown below the field to provide additional context + */ + help_text: string | null + /** + * Whether to hide this field in list views + */ + hide_in_lists: boolean | null + /** + * Layout width in grid units (1-12, where 12 is full width) + */ + width: number | null + /** + * Field display order in forms (lower numbers appear first) + */ + order: number | null + /** + * Group name for organizing fields into sections + */ + group: string | null + /** + * Custom CSS class to apply to the field container + */ + css_class: string | null + /** + * Configuration for WYSIWYG editor toolbar (for Wysiwyg fields) + */ + wysiwyg_toolbar: string | null + /** + * HTML input type attribute (e.g., "password", "email", "tel") + */ + input_type: string | null +} diff --git a/fe/src/types/generated/UpdateEmailTemplateRequest.ts b/fe/src/types/generated/UpdateEmailTemplateRequest.ts index d5731b40..8aba1d3b 100644 --- a/fe/src/types/generated/UpdateEmailTemplateRequest.ts +++ b/fe/src/types/generated/UpdateEmailTemplateRequest.ts @@ -3,24 +3,25 @@ /** * Request body for updating an email template */ -export type UpdateEmailTemplateRequest = { -/** - * New display name (only honoured for workflow templates) - */ -name: string | null, -/** - * Updated subject line - */ -subject_template: string, -/** - * Updated HTML body - */ -body_html_template: string, -/** - * Updated plain-text body - */ -body_text_template: string, -/** - * Updated variables schema - */ -variables: unknown, }; +export type UpdateEmailTemplateRequest = { + /** + * New display name (only honoured for workflow templates) + */ + name: string | null + /** + * Updated subject line + */ + subject_template: string + /** + * Updated HTML body + */ + body_html_template: string + /** + * Updated plain-text body + */ + body_text_template: string + /** + * Updated variables schema + */ + variables: unknown +} diff --git a/fe/src/types/generated/UpdateRoleRequest.ts b/fe/src/types/generated/UpdateRoleRequest.ts index 238a4c9c..1071e607 100644 --- a/fe/src/types/generated/UpdateRoleRequest.ts +++ b/fe/src/types/generated/UpdateRoleRequest.ts @@ -1,23 +1,24 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionResponse } from "./PermissionResponse"; +import type { PermissionResponse } from './PermissionResponse' /** * Request to update an existing role */ -export type UpdateRoleRequest = { -/** - * Name of the role - */ -name: string, -/** - * Optional description - */ -description: string | null, -/** - * Whether this role grants super admin privileges - */ -super_admin: boolean | null, -/** - * Direct permissions for this role - */ -permissions: Array, }; +export type UpdateRoleRequest = { + /** + * Name of the role + */ + name: string + /** + * Optional description + */ + description: string | null + /** + * Whether this role grants super admin privileges + */ + super_admin: boolean | null + /** + * Direct permissions for this role + */ + permissions: Array +} diff --git a/fe/src/types/generated/UpdateSettingsBody.ts b/fe/src/types/generated/UpdateSettingsBody.ts new file mode 100644 index 00000000..8553732f --- /dev/null +++ b/fe/src/types/generated/UpdateSettingsBody.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Request body for updating settings + */ +export type UpdateSettingsBody = { + /** + * Whether versioning is enabled + */ + enabled: boolean | null + /** + * Maximum number of versions to keep + */ + max_versions: number | null + /** + * Maximum age in days + */ + max_age_days: number | null +} diff --git a/fe/src/types/generated/UpdateUserRequest.ts b/fe/src/types/generated/UpdateUserRequest.ts index 37839540..868e4a2f 100644 --- a/fe/src/types/generated/UpdateUserRequest.ts +++ b/fe/src/types/generated/UpdateUserRequest.ts @@ -3,32 +3,33 @@ /** * Update user request */ -export type UpdateUserRequest = { -/** - * Email address (optional) - */ -email: string | null, -/** - * Password (optional, only set if provided) - */ -password: string | null, -/** - * First name (optional) - */ -first_name: string | null, -/** - * Last name (optional) - */ -last_name: string | null, -/** - * Role UUIDs to assign to this user (optional) - */ -role_uuids: string[] | null, -/** - * Whether user is active (optional) - */ -is_active: boolean | null, -/** - * Super admin flag (optional) - */ -super_admin: boolean | null, }; +export type UpdateUserRequest = { + /** + * Email address (optional) + */ + email: string | null + /** + * Password (optional, only set if provided) + */ + password: string | null + /** + * First name (optional) + */ + first_name: string | null + /** + * Last name (optional) + */ + last_name: string | null + /** + * Role UUIDs to assign to this user (optional) + */ + role_uuids: string[] | null + /** + * Whether user is active (optional) + */ + is_active: boolean | null + /** + * Super admin flag (optional) + */ + super_admin: boolean | null +} diff --git a/fe/src/types/generated/UpdateWorkflowRequest.ts b/fe/src/types/generated/UpdateWorkflowRequest.ts new file mode 100644 index 00000000..3cdf55ad --- /dev/null +++ b/fe/src/types/generated/UpdateWorkflowRequest.ts @@ -0,0 +1,35 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Request to update an existing workflow + */ +export type UpdateWorkflowRequest = { + /** + * Workflow name + */ + name: string + /** + * Workflow description + */ + description: string | null + /** + * Workflow kind (consumer or provider) + */ + kind: string + /** + * Whether the workflow is enabled + */ + enabled: boolean + /** + * Cron schedule for the workflow + */ + schedule_cron: string | null + /** + * Workflow configuration + */ + config: unknown + /** + * Whether versioning is disabled + */ + versioning_disabled: boolean +} diff --git a/fe/src/types/generated/UpdateWorkflowRunLogSettingsBody.ts b/fe/src/types/generated/UpdateWorkflowRunLogSettingsBody.ts new file mode 100644 index 00000000..483e3086 --- /dev/null +++ b/fe/src/types/generated/UpdateWorkflowRunLogSettingsBody.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Request body for updating workflow run log settings + */ +export type UpdateWorkflowRunLogSettingsBody = { + /** + * Whether pruning is enabled + */ + enabled: boolean | null + /** + * Maximum number of runs to keep per workflow + */ + max_runs: number | null + /** + * Maximum age in days + */ + max_age_days: number | null +} diff --git a/fe/src/types/generated/UserPermissionsResponse.ts b/fe/src/types/generated/UserPermissionsResponse.ts new file mode 100644 index 00000000..583e122e --- /dev/null +++ b/fe/src/types/generated/UserPermissionsResponse.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Response body for `GET /admin/api/v1/auth/permissions` + */ +export type UserPermissionsResponse = { + /** + * Whether the caller is a super admin (all permissions granted) + */ + is_super_admin: boolean + /** + * Flat list of `namespace:permission_type` strings the caller holds + */ + permissions: Array + /** + * Router paths the caller is allowed to navigate to + */ + allowed_routes: Array +} diff --git a/fe/src/types/generated/UserResponse.ts b/fe/src/types/generated/UserResponse.ts index bde59775..239fca8c 100644 --- a/fe/src/types/generated/UserResponse.ts +++ b/fe/src/types/generated/UserResponse.ts @@ -3,68 +3,69 @@ /** * User response DTO (for API serialization) */ -export type UserResponse = { -/** - * User UUID - */ -uuid: string, -/** - * Username - */ -username: string, -/** - * Email address - */ -email: string, -/** - * Full name - */ -full_name: string, -/** - * First name - */ -first_name: string | null, -/** - * Last name - */ -last_name: string | null, -/** - * Role UUIDs assigned to this user - */ -role_uuids: string[], -/** - * User account status - */ -status: string, -/** - * Whether user is active - */ -is_active: boolean, -/** - * Whether user is admin - */ -is_admin: boolean, -/** - * Super admin flag - */ -super_admin: boolean, -/** - * Last login time - */ -last_login: string | null, -/** - * Failed login attempts - */ -failed_login_attempts: number, -/** - * When the user was created - */ -created_at: string, -/** - * When the user was last updated - */ -updated_at: string, -/** - * UUID of the user who created this user - */ -created_by: string, }; +export type UserResponse = { + /** + * User UUID + */ + uuid: string + /** + * Username + */ + username: string + /** + * Email address + */ + email: string + /** + * Full name + */ + full_name: string + /** + * First name + */ + first_name: string | null + /** + * Last name + */ + last_name: string | null + /** + * Role UUIDs assigned to this user + */ + role_uuids: string[] + /** + * User account status + */ + status: string + /** + * Whether user is active + */ + is_active: boolean + /** + * Whether user is admin + */ + is_admin: boolean + /** + * Super admin flag + */ + super_admin: boolean + /** + * Last login time + */ + last_login: string | null + /** + * Failed login attempts + */ + failed_login_attempts: number + /** + * When the user was created + */ + created_at: string + /** + * When the user was last updated + */ + updated_at: string + /** + * UUID of the user who created this user + */ + created_by: string +} diff --git a/fe/src/types/generated/ValidationErrorResponse.ts b/fe/src/types/generated/ValidationErrorResponse.ts index 01e32b28..2fc8d7c7 100644 --- a/fe/src/types/generated/ValidationErrorResponse.ts +++ b/fe/src/types/generated/ValidationErrorResponse.ts @@ -1,15 +1,16 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ValidationViolation } from "./ValidationViolation"; +import type { ValidationViolation } from './ValidationViolation' /** * Validation error response in Symfony format */ -export type ValidationErrorResponse = { -/** - * Overall error message - */ -message: string, -/** - * List of validation violations - */ -violations: Array, }; +export type ValidationErrorResponse = { + /** + * Overall error message + */ + message: string + /** + * List of validation violations + */ + violations: Array +} diff --git a/fe/src/types/generated/ValidationViolation.ts b/fe/src/types/generated/ValidationViolation.ts index e222b00d..1011abd4 100644 --- a/fe/src/types/generated/ValidationViolation.ts +++ b/fe/src/types/generated/ValidationViolation.ts @@ -3,16 +3,17 @@ /** * Individual validation violation for Symfony-style errors */ -export type ValidationViolation = { -/** - * The field that has the validation error - */ -field: string, -/** - * The error message for this field - */ -message: string, -/** - * Optional error code (e.g., `"NOT_BLANK"`, `"NOT_NULL"`) - */ -code: string | null, }; +export type ValidationViolation = { + /** + * The field that has the validation error + */ + field: string + /** + * The error message for this field + */ + message: string + /** + * Optional error code (e.g., `"NOT_BLANK"`, `"NOT_NULL"`) + */ + code: string | null +} diff --git a/fe/src/types/generated/VersionMeta.ts b/fe/src/types/generated/VersionMeta.ts new file mode 100644 index 00000000..f77bebe5 --- /dev/null +++ b/fe/src/types/generated/VersionMeta.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Version metadata for entity versions + */ +export type VersionMeta = { + version_number: number + created_at: string + created_by: string | null + created_by_name: string | null +} diff --git a/fe/src/types/generated/VersionPayload.ts b/fe/src/types/generated/VersionPayload.ts new file mode 100644 index 00000000..def3bdbc --- /dev/null +++ b/fe/src/types/generated/VersionPayload.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Version payload containing the actual entity data + */ +export type VersionPayload = { + version_number: number + created_at: string + created_by: string | null + data: unknown +} diff --git a/fe/src/types/generated/WorkflowDetail.ts b/fe/src/types/generated/WorkflowDetail.ts index eeeeee1b..84f38e1c 100644 --- a/fe/src/types/generated/WorkflowDetail.ts +++ b/fe/src/types/generated/WorkflowDetail.ts @@ -1,3 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WorkflowDetail = { uuid: string, name: string, description: string | null, kind: string, enabled: boolean, schedule_cron: string | null, config: unknown, versioning_disabled: boolean, }; +export type WorkflowDetail = { + uuid: string + name: string + description: string | null + kind: string + enabled: boolean + schedule_cron: string | null + config: unknown + versioning_disabled: boolean +} diff --git a/fe/src/types/generated/WorkflowRunLogDto.ts b/fe/src/types/generated/WorkflowRunLogDto.ts index 0a560a4f..6a2f945e 100644 --- a/fe/src/types/generated/WorkflowRunLogDto.ts +++ b/fe/src/types/generated/WorkflowRunLogDto.ts @@ -1,3 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WorkflowRunLogDto = { uuid: string, ts: string, level: string, message: string, meta: unknown, }; +export type WorkflowRunLogDto = { + uuid: string + ts: string + level: string + message: string + meta: unknown +} diff --git a/fe/src/types/generated/WorkflowRunLogSettingsDto.ts b/fe/src/types/generated/WorkflowRunLogSettingsDto.ts new file mode 100644 index 00000000..ede58f88 --- /dev/null +++ b/fe/src/types/generated/WorkflowRunLogSettingsDto.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * DTO for workflow run log settings (API layer wrapper) + */ +export type WorkflowRunLogSettingsDto = { + /** + * Whether workflow run logs pruning is enabled + */ + enabled: boolean + /** + * Maximum number of runs to keep per workflow + */ + max_runs: number | null + /** + * Maximum age in days for workflow runs + */ + max_age_days: number | null +} diff --git a/fe/src/types/generated/WorkflowRunSummary.ts b/fe/src/types/generated/WorkflowRunSummary.ts index caf86118..c30d2a6c 100644 --- a/fe/src/types/generated/WorkflowRunSummary.ts +++ b/fe/src/types/generated/WorkflowRunSummary.ts @@ -1,3 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WorkflowRunSummary = { uuid: string, status: string, queued_at: string | null, started_at: string | null, finished_at: string | null, processed_items: number | null, failed_items: number | null, }; +export type WorkflowRunSummary = { + uuid: string + status: string + queued_at: string | null + started_at: string | null + finished_at: string | null + processed_items: number | null + failed_items: number | null +} diff --git a/fe/src/types/generated/WorkflowRunUploadResponse.ts b/fe/src/types/generated/WorkflowRunUploadResponse.ts new file mode 100644 index 00000000..15ae3388 --- /dev/null +++ b/fe/src/types/generated/WorkflowRunUploadResponse.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Response for `POST /workflows/{uuid}/run/upload` + */ +export type WorkflowRunUploadResponse = { + /** + * UUID of the newly-created workflow run + */ + run_uuid: string + /** + * Number of items that were staged from the uploaded file + */ + staged_items: number + /** + * Present only when the upload succeeded but the follow-up job enqueue failed + */ + warning: string | null +} diff --git a/fe/src/types/generated/WorkflowStats.ts b/fe/src/types/generated/WorkflowStats.ts new file mode 100644 index 00000000..d3f4438b --- /dev/null +++ b/fe/src/types/generated/WorkflowStats.ts @@ -0,0 +1,16 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WorkflowWithLatestStatus } from './WorkflowWithLatestStatus' + +/** + * Workflow statistics + */ +export type WorkflowStats = { + /** + * Total count of workflows + */ + total: number + /** + * List of workflows with their latest run status + */ + workflows: Array +} diff --git a/fe/src/types/generated/WorkflowSummary.ts b/fe/src/types/generated/WorkflowSummary.ts index 5aa48bb2..53740ed0 100644 --- a/fe/src/types/generated/WorkflowSummary.ts +++ b/fe/src/types/generated/WorkflowSummary.ts @@ -1,7 +1,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WorkflowSummary = { uuid: string, name: string, kind: string, enabled: boolean, schedule_cron: string | null, -/** - * Indicates if this workflow has a from.api source type (accepts POST, cron disabled) - */ -has_api_endpoint: boolean, versioning_disabled: boolean, }; +export type WorkflowSummary = { + uuid: string + name: string + kind: string + enabled: boolean + schedule_cron: string | null + /** + * Indicates if this workflow has a from.api source type (accepts POST, cron disabled) + */ + has_api_endpoint: boolean + versioning_disabled: boolean +} diff --git a/fe/src/types/generated/WorkflowVersionMeta.ts b/fe/src/types/generated/WorkflowVersionMeta.ts index ce31102a..4aabe95b 100644 --- a/fe/src/types/generated/WorkflowVersionMeta.ts +++ b/fe/src/types/generated/WorkflowVersionMeta.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WorkflowVersionMeta = { version_number: number, created_at: string, created_by: string | null, created_by_name: string | null, }; +export type WorkflowVersionMeta = { + version_number: number + created_at: string + created_by: string | null + created_by_name: string | null +} diff --git a/fe/src/types/generated/WorkflowVersionPayload.ts b/fe/src/types/generated/WorkflowVersionPayload.ts index 7f660b2e..1eef7ff8 100644 --- a/fe/src/types/generated/WorkflowVersionPayload.ts +++ b/fe/src/types/generated/WorkflowVersionPayload.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WorkflowVersionPayload = { version_number: number, created_at: string, created_by: string | null, data: unknown, }; +export type WorkflowVersionPayload = { + version_number: number + created_at: string + created_by: string | null + data: unknown +} diff --git a/fe/src/types/generated/WorkflowWithLatestStatus.ts b/fe/src/types/generated/WorkflowWithLatestStatus.ts new file mode 100644 index 00000000..413a991c --- /dev/null +++ b/fe/src/types/generated/WorkflowWithLatestStatus.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Workflow with its latest run status + */ +export type WorkflowWithLatestStatus = { + /** + * Workflow UUID + */ + uuid: string + /** + * Workflow name + */ + name: string + /** + * Latest run status (if any runs exist) + */ + latest_status: string | null +} diff --git a/fe/src/types/generated/validation.ts b/fe/src/types/generated/validation.ts index aa1933b2..32eb11d8 100644 --- a/fe/src/types/generated/validation.ts +++ b/fe/src/types/generated/validation.ts @@ -1,11 +1,11 @@ // AUTO-GENERATED by `rdt generate-ts` -- do not edit -export const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +export const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ -export const USERNAME_MIN_LENGTH = 3; -export const USERNAME_MAX_LENGTH = 50; -export const PASSWORD_MIN_LENGTH = 8; -export const NAME_MIN_LENGTH = 1; -export const API_KEY_NAME_MIN_LENGTH = 1; -export const CSV_DELIMITER_LENGTH = 1; -export const DSL_STEPS_MIN_COUNT = 1; +export const USERNAME_MIN_LENGTH = 3 +export const USERNAME_MAX_LENGTH = 50 +export const PASSWORD_MIN_LENGTH = 8 +export const NAME_MIN_LENGTH = 1 +export const API_KEY_NAME_MIN_LENGTH = 1 +export const CSV_DELIMITER_LENGTH = 1 +export const DSL_STEPS_MIN_COUNT = 1 diff --git a/fe/src/types/schemas/api-key.ts b/fe/src/types/schemas/api-key.ts index d64ab331..f4d85081 100644 --- a/fe/src/types/schemas/api-key.ts +++ b/fe/src/types/schemas/api-key.ts @@ -1,19 +1,19 @@ import { z } from 'zod' import { UuidSchema } from './base' +import type { CreateApiKeyRequest as GeneratedCreateApiKeyRequest } from '../generated/CreateApiKeyRequest' +import type { ReassignApiKeyRequest as GeneratedReassignApiKeyRequest } from '../generated/ReassignApiKeyRequest' import { API_KEY_NAME_MIN_LENGTH } from '../generated/validation' // Create API key request schema (form validation) -// Note: satisfies z.ZodType not applied because the generated -// type uses `bigint | null` for expires_in_days whereas Zod uses `number` (JS has no bigint in Zod). export const CreateApiKeyRequestSchema = z.object({ name: z.string().min(API_KEY_NAME_MIN_LENGTH), - description: z.string().optional(), - expires_in_days: z.number().int().positive().optional(), -}) + description: z.string().nullable(), + expires_in_days: z.number().int().positive().nullable(), +}) satisfies z.ZodType export const ReassignApiKeyRequestSchema = z.object({ user_uuid: UuidSchema, -}) +}) satisfies z.ZodType export const ReassignApiKeyResponseSchema = z.object({ message: z.string(), diff --git a/fe/src/types/schemas/auth.ts b/fe/src/types/schemas/auth.ts index aaa98553..822b548f 100644 --- a/fe/src/types/schemas/auth.ts +++ b/fe/src/types/schemas/auth.ts @@ -1,5 +1,7 @@ import { z } from 'zod' import type { AdminLoginRequest } from '../generated/AdminLoginRequest' +import type { RefreshTokenRequest as GeneratedRefreshTokenRequest } from '../generated/RefreshTokenRequest' +import type { LogoutRequest as GeneratedLogoutRequest } from '../generated/LogoutRequest' import { USERNAME_MIN_LENGTH, PASSWORD_MIN_LENGTH } from '../generated/validation' // Auth schemas (form validation — kept as Zod for runtime validation) @@ -10,11 +12,11 @@ export const LoginRequestSchema = z.object({ export const RefreshTokenRequestSchema = z.object({ refresh_token: z.string(), -}) +}) satisfies z.ZodType export const LogoutRequestSchema = z.object({ refresh_token: z.string(), -}) +}) satisfies z.ZodType // Type exports — response types re-exported from generated for consumers that only need types export type LoginRequest = z.infer diff --git a/fe/src/types/schemas/base.ts b/fe/src/types/schemas/base.ts index 78028ff4..4c7b2de4 100644 --- a/fe/src/types/schemas/base.ts +++ b/fe/src/types/schemas/base.ts @@ -1,4 +1,6 @@ import { z } from 'zod' +import type { ValidationViolation } from '../generated/ValidationViolation' +import type { ValidationErrorResponse } from '../generated/ValidationErrorResponse' // Base schemas for common patterns - UUID v7 only export const UuidSchema = z.string().refine( @@ -9,18 +11,6 @@ export const UuidSchema = z.string().refine( { message: 'Invalid UUID (must be v7)' } ) -// Nullable UUID schema that transforms nil UUIDs to null -export const NullableUuidSchema = z.preprocess( - val => { - // Transform nil UUID (00000000-0000-0000-0000-000000000000) to null - if (val === '00000000-0000-0000-0000-000000000000' || val === null || val === undefined) { - return null - } - return val - }, - z.union([UuidSchema, z.null()]) -) - // Timestamps export const TimestampSchema = z.string().refine( val => { @@ -30,49 +20,20 @@ export const TimestampSchema = z.string().refine( { message: 'Invalid timestamp format, expected ISO 8601' } ) -export const NullableTimestampSchema = z - .string() - .refine( - val => { - const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/ - return isoRegex.test(val) && !isNaN(Date.parse(val)) - }, - { message: 'Invalid timestamp format, expected ISO 8601' } - ) - .nullable() - -// Validation error schemas — kept as Zod because they are used for runtime parsing -// (.parse()) in api/clients/base.ts and api/http-client.ts. -// Generated equivalents: ValidationViolation, ValidationErrorResponse in generated/ +// Zod runtime parsers bound to generated BE types via `satisfies` — the FE cannot +// diverge from the backend contract because a shape mismatch fails compilation. export const ValidationViolationSchema = z.object({ field: z.string(), message: z.string(), - code: z.string().optional(), -}) + code: z.string().nullable(), +}) satisfies z.ZodType export const ValidationErrorResponseSchema = z.object({ message: z.string(), violations: z.array(ValidationViolationSchema), -}) - -// Pagination and meta types (plain TypeScript — no Zod parsing needed) -export interface Pagination { - total: number - page: number - per_page: number - total_pages: number - has_previous: boolean - has_next: boolean -} - -export interface Meta { - pagination?: Pagination - request_id?: string - timestamp?: string - custom?: unknown -} +}) satisfies z.ZodType -// Type re-exports from generated — for consumers that only need the type, not runtime parsing +// Type re-exports — response-envelope shapes come straight from the generated BE bindings. export type { ValidationViolation } from '../generated/ValidationViolation' export type { ValidationErrorResponse } from '../generated/ValidationErrorResponse' export type { PaginationMeta } from '../generated/PaginationMeta' diff --git a/fe/src/types/schemas/dsl.ts b/fe/src/types/schemas/dsl.ts index d82a8636..1fa16946 100644 --- a/fe/src/types/schemas/dsl.ts +++ b/fe/src/types/schemas/dsl.ts @@ -6,6 +6,11 @@ import type { OnComplete } from '../generated/OnComplete' import type { PostRunAction } from '../generated/PostRunAction' import type { PostRunCondition } from '../generated/PostRunCondition' import type { PostRunSendEmail } from '../generated/PostRunSendEmail' +import type { StringOperand as GeneratedStringOperand } from '../generated/StringOperand' +import type { SendEmailTransform as GeneratedSendEmailTransform } from '../generated/SendEmailTransform' +import type { DslFieldSpec as GeneratedDslFieldSpec } from '../generated/DslFieldSpec' +import type { DslTypeSpec as GeneratedDslTypeSpec } from '../generated/DslTypeSpec' +import type { DslValidateRequest as GeneratedDslValidateRequest } from '../generated/DslValidateRequest' export type { OnComplete, PostRunAction, PostRunCondition, PostRunSendEmail } @@ -156,7 +161,7 @@ export const DslStringOperandConstSchema = z.object({ export const DslStringOperandSchema = z.discriminatedUnion('kind', [ DslStringOperandFieldSchema, DslStringOperandConstSchema, -]) +]) satisfies z.ZodType export const DslToEmailSchema = z.object({ type: z.literal('email'), @@ -250,9 +255,9 @@ export const DslTransformSendEmailSchema = z.object({ type: z.literal('send_email'), template_uuid: z.string(), to: z.array(DslStringOperandSchema), - cc: z.array(DslStringOperandSchema).optional(), + cc: z.array(DslStringOperandSchema).nullable(), target_status: z.string(), -}) +}) satisfies z.ZodType<{ type: 'send_email' } & GeneratedSendEmailTransform> export const DslTransformSchema = z.discriminatedUnion('type', [ DslTransformNoneSchema, DslTransformArithmeticSchema, @@ -272,7 +277,7 @@ export const DslStepSchema = z.object({ export const DslValidateRequestSchema = z.object({ steps: z.array(DslStepSchema).min(DSL_STEPS_MIN_COUNT), -}) +}) satisfies z.ZodType export const DslValidateResponseSchema = z.object({ valid: z.boolean(), @@ -282,13 +287,13 @@ export const DslFieldSpecSchema = z.object({ name: z.string(), type: z.string(), required: z.boolean(), - options: z.array(z.string()).optional(), -}) + options: z.array(z.string()).nullable(), +}) satisfies z.ZodType export const DslTypeSpecSchema = z.object({ type: z.string(), fields: z.array(DslFieldSpecSchema), -}) +}) satisfies z.ZodType export type DslStep = z.infer export type DslValidateRequest = z.infer diff --git a/fe/src/types/schemas/entity.ts b/fe/src/types/schemas/entity.ts index ed50ed62..39d076e0 100644 --- a/fe/src/types/schemas/entity.ts +++ b/fe/src/types/schemas/entity.ts @@ -1,8 +1,10 @@ import { z } from 'zod' import { UuidSchema, TimestampSchema } from './base' -// Field constraints - API returns nested structure: { type: string, constraints: { ... } } -// We use a permissive type to handle various constraint shapes +// Field constraints — generated FieldConstraints is a discriminated union over field type +// (StringConstraints | NumericConstraints | ...). This Zod schema is deliberately permissive +// to accept any of those shapes without enumerating them; tighten to a discriminatedUnion +// matching generated/FieldConstraints.ts if runtime validation becomes required. export const FieldConstraintsSchema = z .object({ type: z.string().optional(), diff --git a/fe/src/types/schemas/index.ts b/fe/src/types/schemas/index.ts index e6fda697..e937677f 100644 --- a/fe/src/types/schemas/index.ts +++ b/fe/src/types/schemas/index.ts @@ -27,6 +27,7 @@ import type { RefreshTokenResponse, LogoutRequest, } from './auth' +import type { Status } from '../generated/Status' import type { FieldDefinition, EntityDefinition, @@ -46,7 +47,8 @@ import type { ReassignApiKeyResponse, ApiKeyCustomData, } from './api-key' -import type { User, UserCustomData } from './user' +import type { UserCustomData } from './user' +import type { UserResponse } from '../generated/UserResponse' import type { Role, Permission, @@ -67,6 +69,7 @@ export type { SnackbarConfig, DialogConfig, FormField, + Status, LoginRequest, LoginResponse, RefreshTokenRequest, @@ -90,7 +93,7 @@ export type { ReassignApiKeyRequest, ReassignApiKeyResponse, ApiKeyCustomData, - User, + UserResponse, UserCustomData, Role, Permission, diff --git a/fe/src/types/schemas/role.ts b/fe/src/types/schemas/role.ts index 3b79e2cb..9ff568d7 100644 --- a/fe/src/types/schemas/role.ts +++ b/fe/src/types/schemas/role.ts @@ -1,7 +1,14 @@ import { z } from 'zod' import { UuidSchema } from './base' +import type { PermissionType as GeneratedPermissionType } from '../generated/PermissionType' +import type { AccessLevel as GeneratedAccessLevel } from '../generated/AccessLevel' +import type { ResourceNamespace as GeneratedResourceNamespace } from '../generated/ResourceNamespace' +import type { PermissionResponse } from '../generated/PermissionResponse' +import type { CreateRoleRequest as GeneratedCreateRoleRequest } from '../generated/CreateRoleRequest' +import type { UpdateRoleRequest as GeneratedUpdateRoleRequest } from '../generated/UpdateRoleRequest' +import type { AssignRolesRequest as GeneratedAssignRolesRequest } from '../generated/AssignRolesRequest' -// Enums — aligned with generated AccessLevel and PermissionType +// Enums — guarded against drift from generated unions via `satisfies` export const ResourceNamespaceSchema = z.enum([ 'Workflows', 'Entities', @@ -11,7 +18,7 @@ export const ResourceNamespaceSchema = z.enum([ 'Users', 'System', 'DashboardStats', -]) +]) satisfies z.ZodType export const PermissionTypeSchema = z.enum([ 'Read', @@ -21,40 +28,42 @@ export const PermissionTypeSchema = z.enum([ 'Publish', 'Admin', 'Execute', -]) +]) satisfies z.ZodType -export const AccessLevelSchema = z.enum(['None', 'Own', 'Group', 'All']) +export const AccessLevelSchema = z.enum([ + 'None', + 'Own', + 'Group', + 'All', +]) satisfies z.ZodType -// Permission schema — used by form components for creating/updating roles +// Permission schema — binds to generated PermissionResponse (constraints: unknown) export const PermissionSchema = z.object({ - resource_type: z.string(), // ResourceNamespace as string + resource_type: z.string(), permission_type: PermissionTypeSchema, access_level: AccessLevelSchema, resource_uuids: z.array(UuidSchema), - constraints: z.record(z.string(), z.unknown()).nullish(), -}) + constraints: z.unknown(), +}) satisfies z.ZodType -// Request schemas (form validation) -// Note: satisfies z.ZodType not applied because the generated -// type uses `PermissionResponse` (with `constraints: unknown`) while the Zod schema uses -// `constraints: Record | null | undefined` — structurally different. +// Request schemas (form validation) — bind to generated types export const CreateRoleRequestSchema = z.object({ name: z.string(), - description: z.string().nullable().optional(), - super_admin: z.boolean().optional(), + description: z.string().nullable(), + super_admin: z.boolean().nullable(), permissions: z.array(PermissionSchema), -}) +}) satisfies z.ZodType export const UpdateRoleRequestSchema = z.object({ name: z.string(), - description: z.string().nullable().optional(), - super_admin: z.boolean().optional(), + description: z.string().nullable(), + super_admin: z.boolean().nullable(), permissions: z.array(PermissionSchema), -}) +}) satisfies z.ZodType export const AssignRolesRequestSchema = z.object({ role_uuids: z.array(UuidSchema), -}) +}) satisfies z.ZodType // Type exports export type ResourceNamespace = z.infer diff --git a/fe/src/types/schemas/user.ts b/fe/src/types/schemas/user.ts index 22abb340..7436bd2b 100644 --- a/fe/src/types/schemas/user.ts +++ b/fe/src/types/schemas/user.ts @@ -1,5 +1,7 @@ import { z } from 'zod' import { UuidSchema } from './base' +import type { CreateUserRequest as GeneratedCreateUserRequest } from '../generated/CreateUserRequest' +import type { UpdateUserRequest as GeneratedUpdateUserRequest } from '../generated/UpdateUserRequest' import { EMAIL_PATTERN, USERNAME_MIN_LENGTH, @@ -14,50 +16,33 @@ const emailValidation = z .refine(val => EMAIL_PATTERN.test(val), 'Invalid email format') // Create user request schema (form validation) -// Note: satisfies z.ZodType not applied because the generated -// type uses `string[] | null` for optional fields whereas Zod uses `.optional()` — -// the Rust-side serialisation sends null for absent fields; the FE omits them entirely. export const CreateUserRequestSchema = z.object({ username: z.string().min(USERNAME_MIN_LENGTH).max(USERNAME_MAX_LENGTH), email: emailValidation, password: z.string().min(PASSWORD_MIN_LENGTH), first_name: z.string(), last_name: z.string(), - role_uuids: z.array(UuidSchema).optional(), - is_active: z.boolean().optional(), - super_admin: z.boolean().optional(), -}) + role_uuids: z.array(UuidSchema).nullable(), + is_active: z.boolean().nullable(), + super_admin: z.boolean().nullable(), +}) satisfies z.ZodType // Update user request schema (form validation) export const UpdateUserRequestSchema = z.object({ - email: emailValidation.optional(), - password: z.string().min(PASSWORD_MIN_LENGTH).optional(), - first_name: z.string().optional(), - last_name: z.string().optional(), - role_uuids: z.array(UuidSchema).optional(), - is_active: z.boolean().optional(), - super_admin: z.boolean().optional(), -}) + email: emailValidation.nullable(), + password: z.string().min(PASSWORD_MIN_LENGTH).nullable(), + first_name: z.string().nullable(), + last_name: z.string().nullable(), + role_uuids: z.array(UuidSchema).nullable(), + is_active: z.boolean().nullable(), + super_admin: z.boolean().nullable(), +}) satisfies z.ZodType // Type exports — re-exported from generated for consumers that only need types export type { UserResponse } from '../generated/UserResponse' export type CreateUserRequest = z.infer export type UpdateUserRequest = z.infer -// Legacy User type — FE-only shape used in auth store -export interface User { - uuid: string - username: string - email: string - first_name: string - last_name: string - role_uuids: string[] - is_active: boolean - is_admin: boolean - created_at: string - updated_at: string -} - /** * User custom data/metadata * Flexible type for storing custom key-value pairs with users diff --git a/fe/src/types/schemas/workflow.ts b/fe/src/types/schemas/workflow.ts index 338d708a..1366de6e 100644 --- a/fe/src/types/schemas/workflow.ts +++ b/fe/src/types/schemas/workflow.ts @@ -1,19 +1,8 @@ -// Type exports — use generated types where possible, fallback interfaces for bigint/number compat - // Workflow uses generated WorkflowDetail (kind: string — clients apply lowercase) export type { WorkflowDetail as Workflow } from '../generated/WorkflowDetail' -// WorkflowRun: generated WorkflowRunSummary uses bigint for processed_items/failed_items -// (Rust u64 → TS bigint), but JSON transport sends numbers. Use compatible interface. -export interface WorkflowRun { - uuid: string - status: string - queued_at: string | null - started_at?: string | null - finished_at: string | null - processed_items?: number | null - failed_items?: number | null -} +// WorkflowRun aliases the generated WorkflowRunSummary directly. +export type { WorkflowRunSummary as WorkflowRun } from '../generated/WorkflowRunSummary' // WorkflowRunLog uses generated WorkflowRunLogDto export type { WorkflowRunLogDto as WorkflowRunLog } from '../generated/WorkflowRunLogDto' diff --git a/fe/src/utils/cookies.ts b/fe/src/utils/cookies.ts index 369e5873..16a6f507 100644 --- a/fe/src/utils/cookies.ts +++ b/fe/src/utils/cookies.ts @@ -80,20 +80,6 @@ export function deleteCookie(name: string, options: CookieOptions = {}): void { setSecureCookie(name, '', opts) } -/** - * Check if cookies are supported - */ -export function areCookiesSupported(): boolean { - try { - document.cookie = 'test=1' - const supported = document.cookie.indexOf('test=') !== -1 - document.cookie = 'test=1; expires=Thu, 01 Jan 1970 00:00:00 GMT' - return supported - } catch { - return false - } -} - /** * Set refresh token as secure cookie */ diff --git a/fe/tsconfig.json b/fe/tsconfig.json index 03b022de..0ffa8220 100644 --- a/fe/tsconfig.json +++ b/fe/tsconfig.json @@ -39,10 +39,5 @@ "src/**/*.tsx", "src/**/*.vue", "node_modules/@types/node/globals.d.ts" - ], - "references": [ - { - "path": "./tsconfig.node.json" - } ] }