From 7f9fa7119f7317c516047fa65c6e7b58a8ddca16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Mon, 20 Apr 2026 23:13:44 +0200 Subject: [PATCH 01/11] refactor: using more generated FE types and using knip for usage validation --- .githooks/pre-push | 36 +- .github/workflows/ci.yml | 4 + .rusty_dev_tool/config.toml | 8 +- README.md | 9 + crates/api/src/admin/api_keys/models.rs | 8 +- crates/api/src/admin/api_keys/routes.rs | 2 +- crates/api/src/admin/auth/models.rs | 9 +- .../api/src/admin/email_templates/models.rs | 6 +- .../src/admin/entity_definitions/models.rs | 10 +- crates/api/src/admin/permissions/models.rs | 8 + crates/api/src/admin/system/models.rs | 19 +- crates/api/src/admin/users/models.rs | 22 +- crates/api/src/public/entities/models.rs | 19 +- crates/api/src/response.rs | 2 + crates/core/src/permissions/role/mod.rs | 1 + crates/workflow/src/dsl/on_complete.rs | 2 + crates/workflow/src/dsl/transform.rs | 2 + docs/examples/README.md | 26 + docs/examples/docker-compose/README.md | 79 ++ docs/examples/docker-compose/compose.yaml | 126 ++ docs/examples/docker-compose/env.example | 21 + docs/examples/kubernetes/00-namespace.yaml | 7 + docs/examples/kubernetes/01-configmap.yaml | 28 + .../kubernetes/02-secret.yaml.example | 29 + docs/examples/kubernetes/10-postgres.yaml | 81 ++ docs/examples/kubernetes/11-redis.yaml | 68 + .../kubernetes/20-migrations-job.yaml | 31 + docs/examples/kubernetes/30-core.yaml | 107 ++ docs/examples/kubernetes/31-worker.yaml | 47 + docs/examples/kubernetes/32-maintenance.yaml | 51 + docs/examples/kubernetes/40-ingress.yaml | 26 + docs/examples/kubernetes/README.md | 132 ++ .../components/dialog.component.ts | 29 - fe/knip.json | 38 + fe/package-lock.json | 1181 ++++++++++++++++- fe/package.json | 8 +- fe/src/api/clients/api-keys.ts | 19 +- fe/src/api/clients/base.ts | 44 +- fe/src/api/clients/email-templates.ts | 6 +- fe/src/api/clients/entities.ts | 95 +- fe/src/api/clients/entity-definitions.ts | 60 +- fe/src/api/clients/permissions.test.ts | 2 - fe/src/api/clients/roles.test.ts | 2 - fe/src/api/clients/roles.ts | 24 +- fe/src/api/clients/system-logs.ts | 27 +- fe/src/api/clients/users.ts | 19 +- fe/src/api/clients/workflows.ts | 106 +- fe/src/api/http-client.ts | 321 +---- fe/src/api/typed-client.ts | 9 +- fe/src/components/common/DataTable.vue | 139 -- .../entities/EntityCreateDialog.test.ts | 1 + fe/src/components/permissions/RoleDialog.vue | 8 +- .../permissions/RolePermissionsEditor.vue | 97 -- .../components/workflows/dsl/DslToEditor.vue | 2 +- .../dsl/PostRunActionsEditor.test.ts | 9 +- .../workflows/dsl/PostRunActionsEditor.vue | 3 +- .../dsl/SendEmailTransformEditor.vue | 2 +- fe/src/composables/useErrorHandler.ts | 6 - fe/src/composables/useRoles.test.ts | 3 + fe/src/composables/useUsers.test.ts | 3 + fe/src/stores/auth.ts | 17 +- fe/src/types/common.ts | 26 +- fe/src/types/generated/AccessLevel.ts | 2 +- fe/src/types/generated/AdminLoginRequest.ts | 19 +- fe/src/types/generated/AdminLoginResponse.ts | 59 +- .../types/generated/AdminRegisterRequest.ts | 30 - .../types/generated/AdminRegisterResponse.ts | 18 - .../types/generated/ApiKeyCreatedResponse.ts | 91 +- fe/src/types/generated/ApiKeyResponse.ts | 83 +- fe/src/types/generated/ApplySchemaRequest.ts | 13 +- fe/src/types/generated/AssignRolesRequest.ts | 11 +- .../{EmptyRequest.ts => BrowseKind.ts} | 4 +- fe/src/types/generated/BrowseNode.ts | 36 + .../types/generated/CapabilitiesResponse.ts | 19 +- fe/src/types/generated/CreateApiKeyRequest.ts | 27 +- .../generated/CreateEmailTemplateRequest.ts | 51 +- fe/src/types/generated/CreateRoleRequest.ts | 37 +- fe/src/types/generated/CreateUserRequest.ts | 67 +- .../types/generated/CreateWorkflowResponse.ts | 2 +- fe/src/types/generated/DateTimeConstraints.ts | 19 +- fe/src/types/generated/DslFieldSpec.ts | 7 +- .../DslOptionsAndExamplesResponse.ts | 14 +- fe/src/types/generated/DslOptionsResponse.ts | 4 +- fe/src/types/generated/DslTypeSpec.ts | 4 +- fe/src/types/generated/DslValidateRequest.ts | 11 +- fe/src/types/generated/DslValidateResponse.ts | 11 +- .../types/generated/EmailTemplateListQuery.ts | 12 +- .../types/generated/EmailTemplateResponse.ts | 83 +- fe/src/types/generated/EmailTemplateType.ts | 2 +- .../generated/EntityDefinitionListResponse.ts | 15 - .../types/generated/EntityDefinitionSchema.ts | 93 +- .../generated/EntityDefinitionVersionMeta.ts | 7 +- .../EntityDefinitionVersionPayload.ts | 7 +- fe/src/types/generated/FieldConstraints.ts | 24 +- .../types/generated/FieldDefinitionSchema.ts | 97 +- fe/src/types/generated/FieldTypeSchema.ts | 21 +- fe/src/types/generated/HealthData.ts | 35 +- fe/src/types/generated/LogoutRequest.ts | 11 +- fe/src/types/generated/NumericConstraints.ts | 35 +- fe/src/types/generated/OnComplete.ts | 4 +- fe/src/types/generated/OptionsSourceSchema.ts | 24 - fe/src/types/generated/PaginationMeta.ts | 51 +- fe/src/types/generated/PermissionResponse.ts | 47 +- fe/src/types/generated/PermissionType.ts | 9 +- fe/src/types/generated/PostRunAction.ts | 4 +- fe/src/types/generated/PostRunCondition.ts | 2 +- fe/src/types/generated/PostRunSendEmail.ts | 39 +- .../types/generated/ReassignApiKeyRequest.ts | 11 +- fe/src/types/generated/RefreshTokenRequest.ts | 11 +- .../types/generated/RefreshTokenResponse.ts | 35 +- fe/src/types/generated/RelationConstraints.ts | 11 +- fe/src/types/generated/ResourceNamespace.ts | 16 + fe/src/types/generated/ResponseMeta.ts | 37 +- fe/src/types/generated/RoleResponse.ts | 101 +- fe/src/types/generated/SchemaConstraints.ts | 11 +- fe/src/types/generated/SelectConstraints.ts | 11 +- fe/src/types/generated/SelectOptionSchema.ts | 15 - fe/src/types/generated/SendEmailTransform.ts | 37 +- fe/src/types/generated/Status.ts | 2 +- fe/src/types/generated/StringConstraints.ts | 35 +- fe/src/types/generated/StringOperand.ts | 4 +- fe/src/types/generated/SystemLogDto.ts | 75 +- fe/src/types/generated/SystemLogQuery.ts | 70 +- .../types/generated/SystemLogResourceType.ts | 10 +- fe/src/types/generated/SystemLogStatus.ts | 2 +- fe/src/types/generated/SystemLogType.ts | 7 +- fe/src/types/generated/UiSettingsSchema.ts | 75 +- .../generated/UpdateEmailTemplateRequest.ts | 43 +- fe/src/types/generated/UpdateRoleRequest.ts | 37 +- fe/src/types/generated/UpdateUserRequest.ts | 59 +- fe/src/types/generated/UserResponse.ts | 131 +- .../generated/ValidationErrorResponse.ts | 21 +- fe/src/types/generated/ValidationViolation.ts | 27 +- fe/src/types/generated/VersionMeta.ts | 11 + fe/src/types/generated/VersionPayload.ts | 11 + fe/src/types/generated/WorkflowDetail.ts | 11 +- fe/src/types/generated/WorkflowRunLogDto.ts | 8 +- fe/src/types/generated/WorkflowRunSummary.ts | 10 +- fe/src/types/generated/WorkflowSummary.ts | 17 +- fe/src/types/generated/WorkflowVersionMeta.ts | 7 +- .../types/generated/WorkflowVersionPayload.ts | 7 +- fe/src/types/generated/validation.ts | 16 +- fe/src/types/schemas/api-key.ts | 8 +- fe/src/types/schemas/auth.ts | 6 +- fe/src/types/schemas/base.ts | 53 +- fe/src/types/schemas/contract.test.ts | 5 - fe/src/types/schemas/dsl.ts | 17 +- fe/src/types/schemas/entity.ts | 6 +- fe/src/types/schemas/index.ts | 7 +- fe/src/types/schemas/role.ts | 43 +- fe/src/types/schemas/user.ts | 23 +- fe/src/types/schemas/workflow.ts | 15 +- fe/src/utils/cookies.ts | 14 - 153 files changed, 3575 insertions(+), 2207 deletions(-) create mode 100644 docs/examples/README.md create mode 100644 docs/examples/docker-compose/README.md create mode 100644 docs/examples/docker-compose/compose.yaml create mode 100644 docs/examples/docker-compose/env.example create mode 100644 docs/examples/kubernetes/00-namespace.yaml create mode 100644 docs/examples/kubernetes/01-configmap.yaml create mode 100644 docs/examples/kubernetes/02-secret.yaml.example create mode 100644 docs/examples/kubernetes/10-postgres.yaml create mode 100644 docs/examples/kubernetes/11-redis.yaml create mode 100644 docs/examples/kubernetes/20-migrations-job.yaml create mode 100644 docs/examples/kubernetes/30-core.yaml create mode 100644 docs/examples/kubernetes/31-worker.yaml create mode 100644 docs/examples/kubernetes/32-maintenance.yaml create mode 100644 docs/examples/kubernetes/40-ingress.yaml create mode 100644 docs/examples/kubernetes/README.md delete mode 100644 fe/e2e/page-objects/components/dialog.component.ts create mode 100644 fe/knip.json delete mode 100644 fe/src/components/common/DataTable.vue delete mode 100644 fe/src/components/permissions/RolePermissionsEditor.vue delete mode 100644 fe/src/types/generated/AdminRegisterRequest.ts delete mode 100644 fe/src/types/generated/AdminRegisterResponse.ts rename fe/src/types/generated/{EmptyRequest.ts => BrowseKind.ts} (50%) create mode 100644 fe/src/types/generated/BrowseNode.ts delete mode 100644 fe/src/types/generated/EntityDefinitionListResponse.ts delete mode 100644 fe/src/types/generated/OptionsSourceSchema.ts create mode 100644 fe/src/types/generated/ResourceNamespace.ts delete mode 100644 fe/src/types/generated/SelectOptionSchema.ts create mode 100644 fe/src/types/generated/VersionMeta.ts create mode 100644 fe/src/types/generated/VersionPayload.ts diff --git a/.githooks/pre-push b/.githooks/pre-push index 9fa373a0..10fc9eee 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -24,6 +24,10 @@ 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}" @@ -130,8 +134,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_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_SKIP " for f in "${files[@]}"; do local path="${root}/${f}" @@ -390,6 +394,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 +463,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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11eed9db..266d7df5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -381,6 +381,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' }} 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..1ed83578 100644 --- a/crates/api/src/admin/api_keys/models.rs +++ b/crates/api/src/admin/api_keys/models.rs @@ -13,11 +13,13 @@ pub struct CreateApiKeyRequest { /// Name of the API key pub name: String, /// Optional description for the API key + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, /// Number of days until expiration (default: 365) - #[serde(default)] - #[ts(type = "number | null")] - pub expires_in_days: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + 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..8f86648e 100644 --- a/crates/api/src/admin/auth/models.rs +++ b/crates/api/src/admin/auth/models.rs @@ -8,8 +8,7 @@ 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 {} /// Refresh token request body @@ -93,8 +92,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 +119,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/email_templates/models.rs b/crates/api/src/admin/email_templates/models.rs index 72515861..86da87fd 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", skip_serializing_if = "Option::is_none")] + #[ts(optional, rename = "type")] pub template_type: Option, } @@ -40,6 +40,8 @@ pub struct CreateEmailTemplateRequest { #[ts(export)] pub struct UpdateEmailTemplateRequest { /// New display name (only honoured for workflow templates) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] 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..3d6420a0 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, } @@ -313,8 +309,8 @@ pub struct EntityDefinitionListResponse { pub struct ApplySchemaRequest { /// Optional UUID of specific entity definition to apply schema for /// If not provided, schemas for all published entity definitions will be applied - #[serde(default)] - #[ts(type = "string | null")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional, type = "string")] pub uuid: Option, } diff --git a/crates/api/src/admin/permissions/models.rs b/crates/api/src/admin/permissions/models.rs index a15711da..ed8a411b 100644 --- a/crates/api/src/admin/permissions/models.rs +++ b/crates/api/src/admin/permissions/models.rs @@ -140,8 +140,12 @@ pub struct CreateRoleRequest { /// Name of the role pub name: String, /// Optional description + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, /// Whether this role grants super admin privileges + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub super_admin: Option, /// Direct permissions for this role pub permissions: Vec, @@ -154,8 +158,12 @@ pub struct UpdateRoleRequest { /// Name of the role pub name: String, /// Optional description + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, /// Whether this role grants super admin privileges + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] 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..dcd5e5e5 100644 --- a/crates/api/src/admin/system/models.rs +++ b/crates/api/src/admin/system/models.rs @@ -190,23 +190,36 @@ pub struct CapabilitiesResponse { #[ts(export)] pub struct SystemLogQuery { /// Page number (1-based, default: 1) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional, type = "number")] pub page: Option, /// Items per page (default: 20, max: 100) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional, type = "number")] pub page_size: Option, /// Filter by log type - #[ts(type = "string | null")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub log_type: Option, /// Filter by resource type - #[ts(type = "string | null")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub resource_type: Option, /// Filter by status - #[ts(type = "string | null")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub status: Option, /// Filter by resource UUID + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub resource_uuid: Option, /// Filter logs created after this timestamp (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub date_from: Option, /// Filter logs created before this timestamp (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub date_to: Option, } diff --git a/crates/api/src/admin/users/models.rs b/crates/api/src/admin/users/models.rs index 8ee8fd3c..c6a7c040 100644 --- a/crates/api/src/admin/users/models.rs +++ b/crates/api/src/admin/users/models.rs @@ -100,11 +100,16 @@ pub struct CreateUserRequest { /// Last name pub last_name: String, /// Role UUIDs to assign to this user (optional) - #[ts(type = "string[] | null")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional, type = "string[]")] pub role_uuids: Option>, /// Whether user is active + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub is_active: Option, /// Super admin flag + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub super_admin: Option, } @@ -114,20 +119,33 @@ pub struct CreateUserRequest { pub struct UpdateUserRequest { /// Email address (optional) #[validate(regex(path = *EMAIL_RE))] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub email: Option, /// Password (optional, only set if provided) #[validate(length(min = 8))] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub password: Option, /// First name (optional) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub first_name: Option, /// Last name (optional) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub last_name: Option, /// Role UUIDs to assign to this user (optional) - #[ts(type = "string[] | null")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional, type = "string[]")] pub role_uuids: Option>, /// Whether user is active (optional) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub is_active: Option, /// Super admin flag (optional) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub super_admin: Option, } 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/response.rs b/crates/api/src/response.rs index 68938f66..5bc38f1e 100644 --- a/crates/api/src/response.rs +++ b/crates/api/src/response.rs @@ -15,6 +15,8 @@ pub struct ValidationViolation { /// The error message for this field pub message: String, /// Optional error code (e.g., `"NOT_BLANK"`, `"NOT_NULL"`) + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] 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/workflow/src/dsl/on_complete.rs b/crates/workflow/src/dsl/on_complete.rs index 7f3b441f..a73b2768 100644 --- a/crates/workflow/src/dsl/on_complete.rs +++ b/crates/workflow/src/dsl/on_complete.rs @@ -31,6 +31,8 @@ pub struct PostRunSendEmail { /// Recipients (only `const_string` — no field refs in post-run context) pub to: Vec, /// Optional CC + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] 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..90aa652b 100644 --- a/crates/workflow/src/dsl/transform.rs +++ b/crates/workflow/src/dsl/transform.rs @@ -161,6 +161,8 @@ pub struct SendEmailTransform { /// Recipients: field refs or constant email addresses pub to: Vec, /// Optional CC recipients + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] 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..1d77514f --- /dev/null +++ b/docs/examples/docker-compose/compose.yaml @@ -0,0 +1,126 @@ +# RDataCore — Docker Compose example (production-flavored 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: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..4b3c7a3d --- /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..25473c41 --- /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" + CORS_ORIGINS: "*" + JWT_EXPIRATION: "86400" + 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..28d8c887 --- /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..c7bd3c52 --- /dev/null +++ b/docs/examples/kubernetes/10-postgres.yaml @@ -0,0 +1,81 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: rdatacore + labels: + app: postgres +spec: + clusterIP: None # Headless — StatefulSet stable DNS + ports: + - name: postgres + port: 5432 + targetPort: 5432 + selector: + app: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: rdatacore +spec: + serviceName: postgres + 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..7c59cd34 --- /dev/null +++ b/docs/examples/kubernetes/11-redis.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: rdatacore + labels: + app: redis +spec: + clusterIP: None + ports: + - name: redis + port: 6379 + targetPort: 6379 + selector: + app: redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + namespace: rdatacore +spec: + serviceName: redis + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:8-alpine + args: ["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..d2bede37 --- /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..8184b8a6 --- /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..7b27677b --- /dev/null +++ b/docs/examples/kubernetes/31-worker.yaml @@ -0,0 +1,47 @@ +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 + 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..95104e36 --- /dev/null +++ b/docs/examples/kubernetes/32-maintenance.yaml @@ -0,0 +1,51 @@ +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 + 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..ac0ea632 --- /dev/null +++ b/docs/examples/kubernetes/40-ingress.yaml @@ -0,0 +1,26 @@ +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 + 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..db5d6b95 --- /dev/null +++ b/docs/examples/kubernetes/README.md @@ -0,0 +1,132 @@ +# 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. +# Ensure DATABASE_URL embeds the same POSTGRES_PASSWORD. +``` + +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' +``` + +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. 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/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..0d42c761 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,6 +229,19 @@ "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", @@ -238,6 +252,18 @@ "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" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1292,21 +1318,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 +2418,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 +3239,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 +3901,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 +3943,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 +3998,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 +4063,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 +4093,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 +4306,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 +4393,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 +4526,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 +4675,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 +4728,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 +4841,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 +5203,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 +5233,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 +5305,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 +5460,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 +5733,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 +5835,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 +6255,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 +6472,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.ts b/fe/src/api/clients/api-keys.ts index f428617d..4b34d8ed 100644 --- a/fe/src/api/clients/api-keys.ts +++ b/fe/src/api/clients/api-keys.ts @@ -1,6 +1,6 @@ import type { ApiKeyResponse } from '@/types/generated/ApiKeyResponse' import type { ApiKeyCreatedResponse } from '@/types/generated/ApiKeyCreatedResponse' -import type { CreateApiKeyRequest, ReassignApiKeyRequest } from '@/types/schemas' +import type { CreateApiKeyRequest, ReassignApiKeyRequest, ResponseMeta } from '@/types/schemas' import { BaseTypedHttpClient } from './base' export class ApiKeysClient extends BaseTypedHttpClient { @@ -9,22 +9,7 @@ export class ApiKeysClient extends BaseTypedHttpClient { 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 - } - }> { + ): Promise<{ data: ApiKeyResponse[]; meta?: ResponseMeta }> { let url = `/admin/api/v1/api-keys?page=${page}&per_page=${itemsPerPage}` if (sortBy && sortOrder) { url += `&sort_by=${sortBy}&sort_order=${sortOrder}` 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/email-templates.ts b/fe/src/api/clients/email-templates.ts index 9c1de647..8bb6ff5f 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 = {}): 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.ts b/fe/src/api/clients/entity-definitions.ts index a146d098..6c54d703 100644 --- a/fe/src/api/clients/entity-definitions.ts +++ b/fe/src/api/clients/entity-definitions.ts @@ -2,29 +2,18 @@ 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 { BaseTypedHttpClient } from './base' 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 - } - }> { + ): Promise<{ data: EntityDefinition[]; meta?: ResponseMeta }> { const pageSize = limit ?? this.getDefaultPageSize() return this.paginatedRequest( `/admin/api/v1/entity-definitions?limit=${pageSize}&offset=${offset}` @@ -60,9 +49,10 @@ 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 } : {} return this.request<{ message: string }>(endpoint, { method: 'POST', - body: JSON.stringify({ uuid }), + body: JSON.stringify(body), }) } @@ -74,38 +64,18 @@ export class EntityDefinitionsClient extends BaseTypedHttpClient { >(`/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/permissions.test.ts b/fe/src/api/clients/permissions.test.ts index 58ce468c..3d1d5d90 100644 --- a/fe/src/api/clients/permissions.test.ts +++ b/fe/src/api/clients/permissions.test.ts @@ -203,7 +203,6 @@ describe('RolesClient (permissions)', () => { it('should handle 422 validation error on create', async () => { const request: CreateRoleRequest = { name: '', - description: null, permissions: [], } @@ -258,7 +257,6 @@ describe('RolesClient (permissions)', () => { it('should handle 404 error when updating non-existent role', async () => { const request: UpdateRoleRequest = { name: 'Updated', - description: null, permissions: [], } diff --git a/fe/src/api/clients/roles.test.ts b/fe/src/api/clients/roles.test.ts index 8c68a8b7..b6628829 100644 --- a/fe/src/api/clients/roles.test.ts +++ b/fe/src/api/clients/roles.test.ts @@ -270,7 +270,6 @@ describe('RolesClient', () => { it('should handle 422 validation error on create', async () => { const request: CreateRoleRequest = { name: '', - description: null, permissions: [], } @@ -324,7 +323,6 @@ describe('RolesClient', () => { it('should handle 404 error when updating non-existent role', async () => { const request: UpdateRoleRequest = { name: 'Updated', - description: null, permissions: [], } diff --git a/fe/src/api/clients/roles.ts b/fe/src/api/clients/roles.ts index c62feb12..447d6ad4 100644 --- a/fe/src/api/clients/roles.ts +++ b/fe/src/api/clients/roles.ts @@ -1,5 +1,10 @@ import type { RoleResponse } from '@/types/generated/RoleResponse' -import type { CreateRoleRequest, UpdateRoleRequest, AssignRolesRequest } from '@/types/schemas' +import type { + CreateRoleRequest, + UpdateRoleRequest, + AssignRolesRequest, + ResponseMeta, +} from '@/types/schemas' import { BaseTypedHttpClient } from './base' export class RolesClient extends BaseTypedHttpClient { @@ -8,22 +13,7 @@ export class RolesClient extends BaseTypedHttpClient { 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 - } - }> { + ): Promise<{ data: RoleResponse[]; meta?: ResponseMeta }> { let url = `/admin/api/v1/roles?page=${page}&per_page=${itemsPerPage}` if (sortBy && sortOrder) { url += `&sort_by=${sortBy}&sort_order=${sortOrder}` diff --git a/fe/src/api/clients/system-logs.ts b/fe/src/api/clients/system-logs.ts index f86f2169..32c51263 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/users.ts b/fe/src/api/clients/users.ts index 86006ed4..9410e668 100644 --- a/fe/src/api/clients/users.ts +++ b/fe/src/api/clients/users.ts @@ -1,5 +1,5 @@ import type { UserResponse } from '@/types/generated/UserResponse' -import type { CreateUserRequest, UpdateUserRequest } from '@/types/schemas' +import type { CreateUserRequest, UpdateUserRequest, ResponseMeta } from '@/types/schemas' import { BaseTypedHttpClient } from './base' export class UsersClient extends BaseTypedHttpClient { @@ -8,22 +8,7 @@ export class UsersClient extends BaseTypedHttpClient { 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 - } - }> { + ): Promise<{ data: UserResponse[]; meta?: ResponseMeta }> { let url = `/admin/api/v1/users?page=${page}&per_page=${itemsPerPage}` if (sortBy && sortOrder) { url += `&sort_by=${sortBy}&sort_order=${sortOrder}` diff --git a/fe/src/api/clients/workflows.ts b/fe/src/api/clients/workflows.ts index a55ef078..b2143270 100644 --- a/fe/src/api/clients/workflows.ts +++ b/fe/src/api/clients/workflows.ts @@ -1,7 +1,10 @@ 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 { WorkflowVersionMeta } from '@/types/generated/WorkflowVersionMeta' +import type { WorkflowVersionPayload } from '@/types/generated/WorkflowVersionPayload' +import type { DslOptionsResponse, WorkflowRun, WorkflowConfig, ResponseMeta } from '@/types/schemas' import { BaseTypedHttpClient } from './base' import { useAuthStore } from '@/stores/auth' import { buildApiUrl } from '@/env-check' @@ -16,19 +19,7 @@ export class WorkflowsClient extends BaseTypedHttpClient { 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 - } - } - }> { + ): Promise<{ data: WorkflowSummary[]; meta?: ResponseMeta }> { let url = `/admin/api/v1/workflows?page=${page}&per_page=${itemsPerPage}` if (sortBy && sortOrder) { url += `&sort_by=${sortBy}&sort_order=${sortOrder}` @@ -48,8 +39,8 @@ export class WorkflowsClient extends BaseTypedHttpClient { schedule_cron?: string | null config: WorkflowConfig versioning_disabled?: boolean - }): Promise<{ uuid: string }> { - return this.request<{ uuid: string }>('/admin/api/v1/workflows', { + }): Promise { + return this.request('/admin/api/v1/workflows', { method: 'POST', body: JSON.stringify(data), }) @@ -95,19 +86,7 @@ export class WorkflowsClient extends BaseTypedHttpClient { 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 - } - } - }> { + ): Promise<{ data: WorkflowRun[]; meta?: ResponseMeta }> { return this.paginatedRequest( `/admin/api/v1/workflows/${workflowUuid}/runs?page=${page}&per_page=${perPage}` ) @@ -117,19 +96,7 @@ export class WorkflowsClient extends BaseTypedHttpClient { 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 - } - } - }> { + ): Promise<{ data: WorkflowRunLogDto[]; meta?: ResponseMeta }> { return this.paginatedRequest( `/admin/api/v1/workflows/runs/${runUuid}/logs?page=${page}&per_page=${perPage}` ) @@ -138,19 +105,7 @@ export class WorkflowsClient extends BaseTypedHttpClient { 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 - } - } - }> { + ): Promise<{ data: WorkflowRun[]; meta?: ResponseMeta }> { return this.paginatedRequest( `/admin/api/v1/workflows/runs?page=${page}&per_page=${perPage}` ) @@ -215,38 +170,13 @@ export class WorkflowsClient extends BaseTypedHttpClient { }) } - 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/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/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/RoleDialog.vue b/fe/src/components/permissions/RoleDialog.vue index 7dd303c7..bd8def5d 100644 --- a/fe/src/components/permissions/RoleDialog.vue +++ b/fe/src/components/permissions/RoleDialog.vue @@ -142,12 +142,12 @@ const formData = ref<{ name: string - description: string | null + description: string | undefined permissions: Permission[] super_admin: boolean }>({ name: '', - description: null, + description: undefined, permissions: [], super_admin: false, }) @@ -164,14 +164,14 @@ if (props.editingRole) { formData.value = { name: props.editingRole.name, - description: props.editingRole.description ?? null, + description: props.editingRole.description ?? undefined, permissions: JSON.parse(JSON.stringify(props.editingRole.permissions)), super_admin: props.editingRole.super_admin, } } else { formData.value = { name: '', - description: null, + description: undefined, permissions: [], super_admin: false, } 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/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/PostRunActionsEditor.test.ts b/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts index 0b6c1849..6c5d2ed2 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), }, })) @@ -78,7 +78,6 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'admin@example.com' }], - cc: null, condition: 'always', }, ], @@ -128,7 +127,6 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'user@example.com' }], - cc: null, condition: 'on_success', }, ], @@ -160,7 +158,6 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'admin@example.com' }], - cc: null, condition: 'always', }, ], @@ -194,7 +191,6 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'admin@example.com' }], - cc: null, condition: 'always', }, ], @@ -231,7 +227,7 @@ describe('PostRunActionsEditor', () => { await nextTick() await nextTick() - expect(mockListEmailTemplates).toHaveBeenCalledWith('workflow') + expect(mockListEmailTemplates).toHaveBeenCalledWith({ type: 'workflow' }) }) it('handles template loading failure gracefully', async () => { @@ -260,7 +256,6 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: '', to: [], - cc: null, condition: 'always', }, ], diff --git a/fe/src/components/workflows/dsl/PostRunActionsEditor.vue b/fe/src/components/workflows/dsl/PostRunActionsEditor.vue index 7e5a9e33..9b593824 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, @@ -220,7 +220,6 @@ type: 'send_email', template_uuid: '', to: [], - cc: null, condition: 'always', } emitUpdated([...actions.value, newAction]) diff --git a/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue b/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue index 4902faa7..9b53f59b 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, diff --git a/fe/src/composables/useErrorHandler.ts b/fe/src/composables/useErrorHandler.ts index 16528eac..ccf89afd 100644 --- a/fe/src/composables/useErrorHandler.ts +++ b/fe/src/composables/useErrorHandler.ts @@ -3,12 +3,6 @@ import { useTranslations } from './useTranslations' import { ValidationError } from '@/api/typed-client' import { HttpError } from '@/api/errors' -export interface FieldError { - field: string - message: string - code?: string -} - 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..5ed8b6c5 100644 --- a/fe/src/composables/useRoles.test.ts +++ b/fe/src/composables/useRoles.test.ts @@ -64,6 +64,9 @@ describe('useRoles', () => { has_previous: false, has_next: false, }, + request_id: null, + timestamp: null, + custom: null, }, }) diff --git a/fe/src/composables/useUsers.test.ts b/fe/src/composables/useUsers.test.ts index e4d821e2..0f2d2578 100644 --- a/fe/src/composables/useUsers.test.ts +++ b/fe/src/composables/useUsers.test.ts @@ -70,6 +70,9 @@ describe('useUsers', () => { has_previous: false, has_next: false, }, + request_id: null, + timestamp: null, + custom: null, }, }) diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index f2da8c30..e640a4f7 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -7,7 +7,20 @@ 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' + +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 +28,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/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..cddc64fa 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 +} 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/CreateApiKeyRequest.ts b/fe/src/types/generated/CreateApiKeyRequest.ts index e4dc9fab..db412b99 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 + /** + * Number of days until expiration (default: 365) + */ + expires_in_days?: number +} 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..f5a49004 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 + /** + * Whether this role grants super admin privileges + */ + super_admin?: boolean + /** + * 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..6792edb2 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[] + /** + * Whether user is active + */ + is_active?: boolean + /** + * Super admin flag + */ + super_admin?: 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/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..5aed7536 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 +} 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/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/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/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..84c52271 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 + /** + * 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..b33e3ece 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 + /** + * Normalized field to store send result (`"queued"`, `"mail_not_configured"`, or error) + */ + target_status: string +} 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..fc5b1116 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 + /** + * Items per page (default: 20, max: 100) + */ + page_size?: number + /** + * Filter by log type + */ + log_type?: SystemLogType + /** + * Filter by resource type + */ + resource_type?: SystemLogResourceType + /** + * Filter by status + */ + status?: SystemLogStatus + /** + * Filter by resource UUID + */ + resource_uuid?: string + /** + * Filter logs created after this timestamp (ISO 8601) + */ + date_from?: string + /** + * Filter logs created before this timestamp (ISO 8601) + */ + date_to?: string +} 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/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..337adbdd 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 + /** + * 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..22e52f37 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 + /** + * Whether this role grants super admin privileges + */ + super_admin?: boolean + /** + * Direct permissions for this role + */ + permissions: Array +} diff --git a/fe/src/types/generated/UpdateUserRequest.ts b/fe/src/types/generated/UpdateUserRequest.ts index 37839540..250e1c9d 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 + /** + * Password (optional, only set if provided) + */ + password?: string + /** + * First name (optional) + */ + first_name?: string + /** + * Last name (optional) + */ + last_name?: string + /** + * Role UUIDs to assign to this user (optional) + */ + role_uuids?: string[] + /** + * Whether user is active (optional) + */ + is_active?: boolean + /** + * Super admin flag (optional) + */ + super_admin?: boolean +} 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..4fd97b4d 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 +} 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/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/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/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..c5df67d0 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(), -}) +}) 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..40b269d6 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(), -}) +}) 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/contract.test.ts b/fe/src/types/schemas/contract.test.ts index 79e4b7ab..4c2438aa 100644 --- a/fe/src/types/schemas/contract.test.ts +++ b/fe/src/types/schemas/contract.test.ts @@ -113,9 +113,6 @@ describe('Generated type contract tests', () => { password: 'securepassword123', first_name: 'New', last_name: 'User', - role_uuids: null, - is_active: null, - super_admin: null, }) expect(fixture.username).toBeTruthy() }) @@ -197,7 +194,6 @@ describe('Generated type contract tests', () => { it('CreateApiKeyRequest matches expected request shape', () => { const fixture = assertType({ name: 'Test Key', - description: null, expires_in_days: 365, }) expect(fixture.name).toBe('Test Key') @@ -321,7 +317,6 @@ describe('Generated type contract tests', () => { const fixture = assertType({ field: 'email', message: 'Invalid email format', - code: null, }) expect(fixture.field).toBe('email') }) diff --git a/fe/src/types/schemas/dsl.ts b/fe/src/types/schemas/dsl.ts index d82a8636..ed03b7ce 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'), @@ -252,7 +257,7 @@ export const DslTransformSendEmailSchema = z.object({ to: z.array(DslStringOperandSchema), cc: z.array(DslStringOperandSchema).optional(), 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..d25abd80 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(), + description: z.string().optional(), super_admin: z.boolean().optional(), permissions: z.array(PermissionSchema), -}) +}) satisfies z.ZodType export const UpdateRoleRequestSchema = z.object({ name: z.string(), - description: z.string().nullable().optional(), + description: z.string().optional(), super_admin: z.boolean().optional(), 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..3db9d866 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,9 +16,6 @@ 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, @@ -26,7 +25,7 @@ export const CreateUserRequestSchema = z.object({ role_uuids: z.array(UuidSchema).optional(), is_active: z.boolean().optional(), super_admin: z.boolean().optional(), -}) +}) satisfies z.ZodType // Update user request schema (form validation) export const UpdateUserRequestSchema = z.object({ @@ -37,27 +36,13 @@ export const UpdateUserRequestSchema = z.object({ role_uuids: z.array(UuidSchema).optional(), is_active: z.boolean().optional(), super_admin: z.boolean().optional(), -}) +}) 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 */ From 91799bf1093ef91d73f5d3a17f7d72bfb6dc07e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Mon, 20 Apr 2026 23:28:12 +0200 Subject: [PATCH 02/11] chore: docs update --- docs/examples/docker-compose/compose.yaml | 232 +++++++++--------- docs/examples/kubernetes/00-namespace.yaml | 8 +- docs/examples/kubernetes/01-configmap.yaml | 48 ++-- .../kubernetes/02-secret.yaml.example | 22 +- docs/examples/kubernetes/10-postgres.yaml | 163 ++++++------ docs/examples/kubernetes/11-redis.yaml | 136 +++++----- .../kubernetes/20-migrations-job.yaml | 54 ++-- docs/examples/kubernetes/30-core.yaml | 174 ++++++------- docs/examples/kubernetes/31-worker.yaml | 84 +++---- docs/examples/kubernetes/32-maintenance.yaml | 92 +++---- docs/examples/kubernetes/40-ingress.yaml | 44 ++-- fe/package-lock.json | 39 ++- 12 files changed, 562 insertions(+), 534 deletions(-) diff --git a/docs/examples/docker-compose/compose.yaml b/docs/examples/docker-compose/compose.yaml index 1d77514f..f6f162b8 100644 --- a/docs/examples/docker-compose/compose.yaml +++ b/docs/examples/docker-compose/compose.yaml @@ -1,126 +1,126 @@ -# RDataCore — Docker Compose example (production-flavored starter) +# 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 + 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 + 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:5432/rdata - profiles: ["migrate"] + 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: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" + 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 + 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 + 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: + postgres_data: + redis_data: diff --git a/docs/examples/kubernetes/00-namespace.yaml b/docs/examples/kubernetes/00-namespace.yaml index 4b3c7a3d..887a90e7 100644 --- a/docs/examples/kubernetes/00-namespace.yaml +++ b/docs/examples/kubernetes/00-namespace.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Namespace metadata: - name: rdatacore - labels: - app.kubernetes.io/name: rdatacore - app.kubernetes.io/part-of: rdatacore + 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 index 25473c41..0958bf1d 100644 --- a/docs/examples/kubernetes/01-configmap.yaml +++ b/docs/examples/kubernetes/01-configmap.yaml @@ -1,28 +1,28 @@ apiVersion: v1 kind: ConfigMap metadata: - name: rdatacore-config - namespace: rdatacore + 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" - CORS_ORIGINS: "*" - JWT_EXPIRATION: "86400" - 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" + 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: "*" + JWT_EXPIRATION: "86400" + 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 index 28d8c887..e6dcb161 100644 --- a/docs/examples/kubernetes/02-secret.yaml.example +++ b/docs/examples/kubernetes/02-secret.yaml.example @@ -14,16 +14,16 @@ apiVersion: v1 kind: Secret metadata: - name: rdatacore-secrets - namespace: rdatacore + 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: "" + 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 index c7bd3c52..33b669f8 100644 --- a/docs/examples/kubernetes/10-postgres.yaml +++ b/docs/examples/kubernetes/10-postgres.yaml @@ -1,81 +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 - namespace: rdatacore - labels: - app: postgres + 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: - clusterIP: None # Headless — StatefulSet stable DNS - ports: - - name: postgres - port: 5432 - targetPort: 5432 - selector: - app: postgres + type: ClusterIP + ports: + - name: postgres + port: 5432 + targetPort: 5432 + selector: + app: postgres --- apiVersion: apps/v1 kind: StatefulSet metadata: - name: postgres - namespace: rdatacore + name: postgres + namespace: rdatacore spec: - serviceName: postgres - 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 + 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 index 7c59cd34..e0c051c6 100644 --- a/docs/examples/kubernetes/11-redis.yaml +++ b/docs/examples/kubernetes/11-redis.yaml @@ -1,68 +1,86 @@ +# Headless Service — governs the StatefulSet, provides stable per-pod DNS. apiVersion: v1 kind: Service metadata: - name: redis - namespace: rdatacore - labels: - app: redis + 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: - clusterIP: None - ports: - - name: redis - port: 6379 - targetPort: 6379 - selector: - app: redis + type: ClusterIP + ports: + - name: redis + port: 6379 + targetPort: 6379 + selector: + app: redis --- apiVersion: apps/v1 kind: StatefulSet metadata: - name: redis - namespace: rdatacore + name: redis + namespace: rdatacore spec: - serviceName: redis - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: redis:8-alpine - args: ["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 + serviceName: redis-headless + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:8-alpine + args: [ "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 index d2bede37..16296ff5 100644 --- a/docs/examples/kubernetes/20-migrations-job.yaml +++ b/docs/examples/kubernetes/20-migrations-job.yaml @@ -1,31 +1,31 @@ apiVersion: batch/v1 kind: Job metadata: - name: rdatacore-migrate - namespace: rdatacore + 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" + 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 index 8184b8a6..a71b9fbf 100644 --- a/docs/examples/kubernetes/30-core.yaml +++ b/docs/examples/kubernetes/30-core.yaml @@ -1,107 +1,107 @@ apiVersion: v1 kind: Service metadata: - name: rdatacore-core - namespace: rdatacore - labels: - app: rdatacore - component: core + 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 + 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: + name: rdatacore-core + namespace: rdatacore + 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" +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 + name: rdatacore-core + namespace: rdatacore spec: - minAvailable: 1 - selector: - matchLabels: - app: rdatacore - component: core + 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 + 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 index 7b27677b..1114673e 100644 --- a/docs/examples/kubernetes/31-worker.yaml +++ b/docs/examples/kubernetes/31-worker.yaml @@ -1,47 +1,47 @@ 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: + name: rdatacore-worker + namespace: rdatacore + 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 - resources: - requests: - cpu: "250m" - memory: "512Mi" - limits: - cpu: "1000m" - memory: "1Gi" +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 + 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 index 95104e36..9098e53f 100644 --- a/docs/examples/kubernetes/32-maintenance.yaml +++ b/docs/examples/kubernetes/32-maintenance.yaml @@ -1,51 +1,51 @@ 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: + name: rdatacore-maintenance + namespace: rdatacore + 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 - resources: - requests: - cpu: "100m" - memory: "256Mi" - limits: - cpu: "500m" - memory: "512Mi" +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 + 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 index ac0ea632..911064f0 100644 --- a/docs/examples/kubernetes/40-ingress.yaml +++ b/docs/examples/kubernetes/40-ingress.yaml @@ -1,26 +1,26 @@ 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 - nginx.ingress.kubernetes.io/proxy-body-size: "16m" + name: rdatacore + namespace: rdatacore + annotations: + # Uncomment and adjust for cert-manager: + # cert-manager.io/cluster-issuer: letsencrypt-prod + 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 + 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/fe/package-lock.json b/fe/package-lock.json index 0d42c761..6c6fff03 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -203,6 +203,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -225,33 +226,11 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "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==", - "dev": true, - "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", @@ -259,7 +238,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2462,6 +2440,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2505,6 +2484,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -2839,6 +2819,7 @@ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, + "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", @@ -3116,6 +3097,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3594,6 +3576,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3653,6 +3636,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5166,6 +5150,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5827,6 +5812,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5882,6 +5868,7 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5978,6 +5965,7 @@ "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.2.tgz", "integrity": "sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==", "devOptional": true, + "peer": true, "dependencies": { "@vuetify/loader-shared": "^2.1.1", "debug": "^4.3.3", @@ -5997,6 +5985,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -6074,6 +6063,7 @@ "version": "3.5.25", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", @@ -6212,6 +6202,7 @@ "version": "3.11.2", "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.2.tgz", "integrity": "sha512-1lL0qN6JIdbx6xGYpo6dnx378EfC0t4EotPJdP4go8ThmIdRO3xLva1ALxhxi5lSYTht4R9OVk9miVnwVfDx3A==", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/johnleider" From 3ab59c60e6c9b84690829f566351171bcff7fbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Mon, 20 Apr 2026 23:57:13 +0200 Subject: [PATCH 03/11] chore: redo the tsbinding ignore --- crates/api/src/admin/api_keys/models.rs | 6 ++-- .../api/src/admin/email_templates/models.rs | 7 ++-- .../src/admin/entity_definitions/models.rs | 4 +-- crates/api/src/admin/permissions/models.rs | 12 +++---- crates/api/src/admin/system/models.rs | 26 ++++++--------- crates/api/src/admin/users/models.rs | 32 +++++++------------ crates/api/src/response.rs | 3 +- crates/workflow/src/dsl/on_complete.rs | 3 +- crates/workflow/src/dsl/transform.rs | 3 +- docs/examples/docker-compose/compose.yaml | 2 +- docs/examples/kubernetes/01-configmap.yaml | 4 +-- docs/examples/kubernetes/11-redis.yaml | 3 +- docs/examples/kubernetes/31-worker.yaml | 11 +++++++ docs/examples/kubernetes/32-maintenance.yaml | 10 ++++++ docs/examples/kubernetes/40-ingress.yaml | 2 ++ docs/examples/kubernetes/README.md | 11 ++++++- fe/src/api/clients/api-keys.test.ts | 2 ++ fe/src/api/clients/email-templates.ts | 2 +- fe/src/api/clients/entity-definitions.test.ts | 2 +- fe/src/api/clients/entity-definitions.ts | 2 +- fe/src/api/clients/permissions.test.ts | 6 ++++ fe/src/api/clients/roles.test.ts | 6 ++++ fe/src/api/clients/system-logs.ts | 13 +++++++- fe/src/api/clients/users.test.ts | 17 ++++++++++ .../api-keys/ApiKeyCreateDialog.vue | 12 +++---- fe/src/components/permissions/RoleDialog.vue | 8 ++--- fe/src/components/system/SystemLogsViewer.vue | 12 +++---- fe/src/components/users/UserDialog.vue | 9 ++---- fe/src/components/workflows/DslClient.test.ts | 4 ++- .../workflows/dsl/DslTransformEditor.vue | 1 + .../dsl/PostRunActionsEditor.test.ts | 5 +++ .../workflows/dsl/PostRunActionsEditor.vue | 1 + .../dsl/SendEmailTransformEditor.vue | 2 +- fe/src/components/workflows/dsl/templates.ts | 1 + fe/src/composables/useApiKeys.test.ts | 12 +++++-- fe/src/composables/useEntities.test.ts | 2 +- fe/src/composables/useErrorHandler.test.ts | 8 ++--- fe/src/composables/useRoles.test.ts | 5 ++- fe/src/composables/useUsers.test.ts | 16 ++++++++++ fe/src/types/generated/ApplySchemaRequest.ts | 2 +- fe/src/types/generated/CreateApiKeyRequest.ts | 4 +-- fe/src/types/generated/CreateRoleRequest.ts | 4 +-- fe/src/types/generated/CreateUserRequest.ts | 6 ++-- .../types/generated/EmailTemplateListQuery.ts | 2 +- fe/src/types/generated/PostRunSendEmail.ts | 2 +- fe/src/types/generated/SendEmailTransform.ts | 2 +- fe/src/types/generated/SystemLogQuery.ts | 16 +++++----- .../generated/UpdateEmailTemplateRequest.ts | 2 +- fe/src/types/generated/UpdateRoleRequest.ts | 4 +-- fe/src/types/generated/UpdateUserRequest.ts | 14 ++++---- fe/src/types/generated/ValidationViolation.ts | 2 +- fe/src/types/schemas/api-key.ts | 4 +-- fe/src/types/schemas/base.ts | 2 +- fe/src/types/schemas/contract.test.ts | 5 +++ fe/src/types/schemas/dsl.ts | 2 +- fe/src/types/schemas/role.ts | 8 ++--- fe/src/types/schemas/user.ts | 20 ++++++------ 57 files changed, 239 insertions(+), 149 deletions(-) diff --git a/crates/api/src/admin/api_keys/models.rs b/crates/api/src/admin/api_keys/models.rs index 1ed83578..338f3db2 100644 --- a/crates/api/src/admin/api_keys/models.rs +++ b/crates/api/src/admin/api_keys/models.rs @@ -13,12 +13,10 @@ pub struct CreateApiKeyRequest { /// Name of the API key pub name: String, /// Optional description for the API key - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub description: Option, /// Number of days until expiration (default: 365) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub expires_in_days: Option, } diff --git a/crates/api/src/admin/email_templates/models.rs b/crates/api/src/admin/email_templates/models.rs index 86da87fd..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(default, rename = "type", skip_serializing_if = "Option::is_none")] - #[ts(optional, rename = "type")] + #[serde(default, rename = "type")] + #[ts(rename = "type")] pub template_type: Option, } @@ -40,8 +40,7 @@ pub struct CreateEmailTemplateRequest { #[ts(export)] pub struct UpdateEmailTemplateRequest { /// New display name (only honoured for workflow templates) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[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 3d6420a0..f3cbc2a2 100644 --- a/crates/api/src/admin/entity_definitions/models.rs +++ b/crates/api/src/admin/entity_definitions/models.rs @@ -309,8 +309,8 @@ pub struct EntityDefinitionListResponse { pub struct ApplySchemaRequest { /// Optional UUID of specific entity definition to apply schema for /// If not provided, schemas for all published entity definitions will be applied - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional, type = "string")] + #[serde(default)] + #[ts(type = "string | null")] pub uuid: Option, } diff --git a/crates/api/src/admin/permissions/models.rs b/crates/api/src/admin/permissions/models.rs index ed8a411b..65e79910 100644 --- a/crates/api/src/admin/permissions/models.rs +++ b/crates/api/src/admin/permissions/models.rs @@ -140,12 +140,10 @@ pub struct CreateRoleRequest { /// Name of the role pub name: String, /// Optional description - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub description: Option, /// Whether this role grants super admin privileges - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub super_admin: Option, /// Direct permissions for this role pub permissions: Vec, @@ -158,12 +156,10 @@ pub struct UpdateRoleRequest { /// Name of the role pub name: String, /// Optional description - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub description: Option, /// Whether this role grants super admin privileges - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[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 dcd5e5e5..19830e0c 100644 --- a/crates/api/src/admin/system/models.rs +++ b/crates/api/src/admin/system/models.rs @@ -190,36 +190,30 @@ pub struct CapabilitiesResponse { #[ts(export)] pub struct SystemLogQuery { /// Page number (1-based, default: 1) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional, type = "number")] + #[serde(default)] + #[ts(type = "number | null")] pub page: Option, /// Items per page (default: 20, max: 100) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional, type = "number")] + #[serde(default)] + #[ts(type = "number | null")] pub page_size: Option, /// Filter by log type - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub log_type: Option, /// Filter by resource type - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub resource_type: Option, /// Filter by status - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub status: Option, /// Filter by resource UUID - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub resource_uuid: Option, /// Filter logs created after this timestamp (ISO 8601) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub date_from: Option, /// Filter logs created before this timestamp (ISO 8601) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub date_to: Option, } diff --git a/crates/api/src/admin/users/models.rs b/crates/api/src/admin/users/models.rs index c6a7c040..e29cfc65 100644 --- a/crates/api/src/admin/users/models.rs +++ b/crates/api/src/admin/users/models.rs @@ -100,16 +100,14 @@ pub struct CreateUserRequest { /// Last name pub last_name: String, /// Role UUIDs to assign to this user (optional) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional, type = "string[]")] + #[serde(default)] + #[ts(type = "string[] | null")] pub role_uuids: Option>, /// Whether user is active - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub is_active: Option, /// Super admin flag - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub super_admin: Option, } @@ -119,33 +117,27 @@ pub struct CreateUserRequest { pub struct UpdateUserRequest { /// Email address (optional) #[validate(regex(path = *EMAIL_RE))] - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub email: Option, /// Password (optional, only set if provided) #[validate(length(min = 8))] - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub password: Option, /// First name (optional) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub first_name: Option, /// Last name (optional) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub last_name: Option, /// Role UUIDs to assign to this user (optional) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional, type = "string[]")] + #[serde(default)] + #[ts(type = "string[] | null")] pub role_uuids: Option>, /// Whether user is active (optional) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub is_active: Option, /// Super admin flag (optional) - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub super_admin: Option, } diff --git a/crates/api/src/response.rs b/crates/api/src/response.rs index 5bc38f1e..fa4f3a5a 100644 --- a/crates/api/src/response.rs +++ b/crates/api/src/response.rs @@ -15,8 +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, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(default)] pub code: Option, } diff --git a/crates/workflow/src/dsl/on_complete.rs b/crates/workflow/src/dsl/on_complete.rs index a73b2768..44ec1857 100644 --- a/crates/workflow/src/dsl/on_complete.rs +++ b/crates/workflow/src/dsl/on_complete.rs @@ -31,8 +31,7 @@ pub struct PostRunSendEmail { /// Recipients (only `const_string` — no field refs in post-run context) pub to: Vec, /// Optional CC - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[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 90aa652b..bc551225 100644 --- a/crates/workflow/src/dsl/transform.rs +++ b/crates/workflow/src/dsl/transform.rs @@ -161,8 +161,7 @@ pub struct SendEmailTransform { /// Recipients: field refs or constant email addresses pub to: Vec, /// Optional CC recipients - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[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/docker-compose/compose.yaml b/docs/examples/docker-compose/compose.yaml index f6f162b8..6781c933 100644 --- a/docs/examples/docker-compose/compose.yaml +++ b/docs/examples/docker-compose/compose.yaml @@ -37,7 +37,7 @@ services: condition: service_healthy entrypoint: [ "/usr/local/bin/run_migrations" ] environment: - DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD}@postgres:5432/rdata + DATABASE_URL: postgres://rdatacore:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}@postgres:5432/rdata profiles: [ "migrate" ] core: diff --git a/docs/examples/kubernetes/01-configmap.yaml b/docs/examples/kubernetes/01-configmap.yaml index 0958bf1d..e088bba0 100644 --- a/docs/examples/kubernetes/01-configmap.yaml +++ b/docs/examples/kubernetes/01-configmap.yaml @@ -11,9 +11,9 @@ data: RUST_LOG: "info" LOG_LEVEL: "info" CACHE_ENABLED: "true" - CACHE_TTL: "300" + CACHE_TTL: "300" # 5 minutes CORS_ORIGINS: "*" - JWT_EXPIRATION: "86400" + JWT_EXPIRATION: "86400" # 24 hours QUEUE_FETCH_KEY: "queue:workflows:fetch" QUEUE_PROCESS_KEY: "queue:workflows:process" QUEUE_EMAIL_KEY: "queue:email" diff --git a/docs/examples/kubernetes/11-redis.yaml b/docs/examples/kubernetes/11-redis.yaml index e0c051c6..3e4afe8c 100644 --- a/docs/examples/kubernetes/11-redis.yaml +++ b/docs/examples/kubernetes/11-redis.yaml @@ -51,7 +51,8 @@ spec: containers: - name: redis image: redis:8-alpine - args: [ "redis-server", "--appendonly", "yes", "--dir", "/data" ] + # Use `command` (not `args`) so this matches the Compose example exactly. + command: [ "redis-server", "--appendonly", "yes", "--dir", "/data" ] ports: - containerPort: 6379 name: redis diff --git a/docs/examples/kubernetes/31-worker.yaml b/docs/examples/kubernetes/31-worker.yaml index 1114673e..d625eb95 100644 --- a/docs/examples/kubernetes/31-worker.yaml +++ b/docs/examples/kubernetes/31-worker.yaml @@ -38,6 +38,17 @@ spec: 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" diff --git a/docs/examples/kubernetes/32-maintenance.yaml b/docs/examples/kubernetes/32-maintenance.yaml index 9098e53f..82b89b03 100644 --- a/docs/examples/kubernetes/32-maintenance.yaml +++ b/docs/examples/kubernetes/32-maintenance.yaml @@ -42,6 +42,16 @@ spec: 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" diff --git a/docs/examples/kubernetes/40-ingress.yaml b/docs/examples/kubernetes/40-ingress.yaml index 911064f0..0d5f989d 100644 --- a/docs/examples/kubernetes/40-ingress.yaml +++ b/docs/examples/kubernetes/40-ingress.yaml @@ -6,6 +6,8 @@ metadata: 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 diff --git a/docs/examples/kubernetes/README.md b/docs/examples/kubernetes/README.md index db5d6b95..a5033956 100644 --- a/docs/examples/kubernetes/README.md +++ b/docs/examples/kubernetes/README.md @@ -44,9 +44,10 @@ If your storage class has a different name, either edit the PVCs in `10-postgres ```bash cp 02-secret.yaml.example 02-secret.yaml # Edit 02-secret.yaml: replace the placeholder values with real secrets. -# Ensure DATABASE_URL embeds the same POSTGRES_PASSWORD. ``` +> **⚠️ 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:** @@ -94,6 +95,13 @@ 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 @@ -130,3 +138,4 @@ This deletes all Deployments/StatefulSets/Services. Longhorn PVs are governed by - **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/src/api/clients/api-keys.test.ts b/fe/src/api/clients/api-keys.test.ts index 976f847f..26822afc 100644 --- a/fe/src/api/clients/api-keys.test.ts +++ b/fe/src/api/clients/api-keys.test.ts @@ -172,6 +172,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/email-templates.ts b/fe/src/api/clients/email-templates.ts index 8bb6ff5f..bd1676a0 100644 --- a/fe/src/api/clients/email-templates.ts +++ b/fe/src/api/clients/email-templates.ts @@ -8,7 +8,7 @@ export type EmailTemplate = EmailTemplateResponse export type { CreateEmailTemplateRequest, UpdateEmailTemplateRequest } export class EmailTemplateClient extends BaseTypedHttpClient { - async list(query: EmailTemplateListQuery = {}): Promise { + 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/entity-definitions.test.ts b/fe/src/api/clients/entity-definitions.test.ts index 0c914469..2ec22f96 100644 --- a/fe/src/api/clients/entity-definitions.test.ts +++ b/fe/src/api/clients/entity-definitions.test.ts @@ -355,7 +355,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 6c54d703..76356827 100644 --- a/fe/src/api/clients/entity-definitions.ts +++ b/fe/src/api/clients/entity-definitions.ts @@ -49,7 +49,7 @@ 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 } : {} + const body: ApplySchemaRequest = { uuid: uuid ?? null } return this.request<{ message: string }>(endpoint, { method: 'POST', body: JSON.stringify(body), diff --git a/fe/src/api/clients/permissions.test.ts b/fe/src/api/clients/permissions.test.ts index 3d1d5d90..059372f2 100644 --- a/fe/src/api/clients/permissions.test.ts +++ b/fe/src/api/clients/permissions.test.ts @@ -167,6 +167,7 @@ describe('RolesClient (permissions)', () => { const request: CreateRoleRequest = { name: 'Auditor', description: 'Read-only auditing role', + super_admin: null, permissions: [mockPermission], } @@ -203,6 +204,8 @@ describe('RolesClient (permissions)', () => { it('should handle 422 validation error on create', async () => { const request: CreateRoleRequest = { name: '', + description: null, + super_admin: null, permissions: [], } @@ -222,6 +225,7 @@ describe('RolesClient (permissions)', () => { const request: UpdateRoleRequest = { name: 'Senior Auditor', description: 'Extended read-only auditing role', + super_admin: null, permissions: [mockPermission], } @@ -257,6 +261,8 @@ describe('RolesClient (permissions)', () => { it('should handle 404 error when updating non-existent role', async () => { const request: UpdateRoleRequest = { name: 'Updated', + description: null, + super_admin: null, permissions: [], } diff --git a/fe/src/api/clients/roles.test.ts b/fe/src/api/clients/roles.test.ts index b6628829..a41ee15d 100644 --- a/fe/src/api/clients/roles.test.ts +++ b/fe/src/api/clients/roles.test.ts @@ -234,6 +234,7 @@ describe('RolesClient', () => { const request: CreateRoleRequest = { name: 'Editor', description: 'Can edit content', + super_admin: null, permissions: [mockPermission], } @@ -270,6 +271,8 @@ describe('RolesClient', () => { it('should handle 422 validation error on create', async () => { const request: CreateRoleRequest = { name: '', + description: null, + super_admin: null, permissions: [], } @@ -289,6 +292,7 @@ describe('RolesClient', () => { const request: UpdateRoleRequest = { name: 'Senior Editor', description: 'Can edit and publish content', + super_admin: null, permissions: [mockPermission], } @@ -323,6 +327,8 @@ describe('RolesClient', () => { it('should handle 404 error when updating non-existent role', async () => { const request: UpdateRoleRequest = { name: 'Updated', + description: null, + super_admin: null, permissions: [], } diff --git a/fe/src/api/clients/system-logs.ts b/fe/src/api/clients/system-logs.ts index 32c51263..b4177822 100644 --- a/fe/src/api/clients/system-logs.ts +++ b/fe/src/api/clients/system-logs.ts @@ -11,7 +11,18 @@ export interface SystemLogListResult { } export class SystemLogClient extends BaseTypedHttpClient { - async list(params: SystemLogQuery = {}): Promise { + async list( + params: SystemLogQuery = { + page: null, + page_size: null, + log_type: null, + resource_type: null, + status: null, + resource_uuid: null, + date_from: null, + date_to: null, + } + ): 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/users.test.ts b/fe/src/api/clients/users.test.ts index 3e906f62..73aeb73e 100644 --- a/fe/src/api/clients/users.test.ts +++ b/fe/src/api/clients/users.test.ts @@ -233,6 +233,9 @@ describe('UsersClient', () => { password: 'securepassword123', first_name: 'New', last_name: 'User', + role_uuids: null, + is_active: null, + super_admin: null, } const mockResponse = { @@ -272,6 +275,9 @@ describe('UsersClient', () => { password: '123', first_name: 'New', last_name: 'User', + role_uuids: null, + is_active: null, + super_admin: null, } mockFetch.mockResolvedValueOnce({ @@ -289,7 +295,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 +334,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/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/permissions/RoleDialog.vue b/fe/src/components/permissions/RoleDialog.vue index bd8def5d..7dd303c7 100644 --- a/fe/src/components/permissions/RoleDialog.vue +++ b/fe/src/components/permissions/RoleDialog.vue @@ -142,12 +142,12 @@ const formData = ref<{ name: string - description: string | undefined + description: string | null permissions: Permission[] super_admin: boolean }>({ name: '', - description: undefined, + description: null, permissions: [], super_admin: false, }) @@ -164,14 +164,14 @@ if (props.editingRole) { formData.value = { name: props.editingRole.name, - description: props.editingRole.description ?? undefined, + description: props.editingRole.description ?? null, permissions: JSON.parse(JSON.stringify(props.editingRole.permissions)), super_admin: props.editingRole.super_admin, } } else { formData.value = { name: '', - description: undefined, + description: null, permissions: [], super_admin: false, } 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/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 6c5d2ed2..eb9fba9d 100644 --- a/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts +++ b/fe/src/components/workflows/dsl/PostRunActionsEditor.test.ts @@ -78,6 +78,7 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'admin@example.com' }], + cc: null, condition: 'always', }, ], @@ -127,6 +128,7 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'user@example.com' }], + cc: null, condition: 'on_success', }, ], @@ -158,6 +160,7 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'admin@example.com' }], + cc: null, condition: 'always', }, ], @@ -191,6 +194,7 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: 'uuid-1', to: [{ kind: 'const_string', value: 'admin@example.com' }], + cc: null, condition: 'always', }, ], @@ -256,6 +260,7 @@ describe('PostRunActionsEditor', () => { type: 'send_email', template_uuid: '', to: [], + cc: null, condition: 'always', }, ], diff --git a/fe/src/components/workflows/dsl/PostRunActionsEditor.vue b/fe/src/components/workflows/dsl/PostRunActionsEditor.vue index 9b593824..72bee452 100644 --- a/fe/src/components/workflows/dsl/PostRunActionsEditor.vue +++ b/fe/src/components/workflows/dsl/PostRunActionsEditor.vue @@ -220,6 +220,7 @@ type: 'send_email', template_uuid: '', to: [], + cc: null, condition: 'always', } emitUpdated([...actions.value, newAction]) diff --git a/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue b/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue index 9b53f59b..97ee748c 100644 --- a/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue +++ b/fe/src/components/workflows/dsl/SendEmailTransformEditor.vue @@ -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..8cf88bb0 100644 --- a/fe/src/composables/useApiKeys.test.ts +++ b/fe/src/composables/useApiKeys.test.ts @@ -132,7 +132,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 +154,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) 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/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/useRoles.test.ts b/fe/src/composables/useRoles.test.ts index 5ed8b6c5..10621ad1 100644 --- a/fe/src/composables/useRoles.test.ts +++ b/fe/src/composables/useRoles.test.ts @@ -113,6 +113,7 @@ describe('useRoles', () => { const updateData: UpdateRoleRequest = { name: 'Updated Role', description: 'Updated description', + super_admin: null, permissions: [], } @@ -178,6 +179,7 @@ describe('useRoles', () => { const updateData: UpdateRoleRequest = { name: 'Updated Role', description: 'Updated description', + super_admin: null, permissions: [], } @@ -224,9 +226,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/useUsers.test.ts b/fe/src/composables/useUsers.test.ts index 0f2d2578..4d5f2039 100644 --- a/fe/src/composables/useUsers.test.ts +++ b/fe/src/composables/useUsers.test.ts @@ -90,6 +90,7 @@ describe('useUsers', () => { password: 'password123', first_name: 'New', last_name: 'User', + role_uuids: null, is_active: true, super_admin: false, } @@ -124,8 +125,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, } @@ -178,6 +182,7 @@ describe('useUsers', () => { password: 'password123', first_name: 'New', last_name: 'User', + role_uuids: null, is_active: true, super_admin: false, } @@ -194,8 +199,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') @@ -280,6 +289,7 @@ describe('useUsers', () => { password: 'password123', first_name: 'New', last_name: 'User', + role_uuids: null, is_active: true, super_admin: false, } @@ -314,6 +324,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/types/generated/ApplySchemaRequest.ts b/fe/src/types/generated/ApplySchemaRequest.ts index cddc64fa..4bf18ff7 100644 --- a/fe/src/types/generated/ApplySchemaRequest.ts +++ b/fe/src/types/generated/ApplySchemaRequest.ts @@ -9,5 +9,5 @@ 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 + uuid: string | null } diff --git a/fe/src/types/generated/CreateApiKeyRequest.ts b/fe/src/types/generated/CreateApiKeyRequest.ts index db412b99..43db8cf2 100644 --- a/fe/src/types/generated/CreateApiKeyRequest.ts +++ b/fe/src/types/generated/CreateApiKeyRequest.ts @@ -11,9 +11,9 @@ export type CreateApiKeyRequest = { /** * Optional description for the API key */ - description?: string + description: string | null /** * Number of days until expiration (default: 365) */ - expires_in_days?: number + expires_in_days: number | null } diff --git a/fe/src/types/generated/CreateRoleRequest.ts b/fe/src/types/generated/CreateRoleRequest.ts index f5a49004..69af0585 100644 --- a/fe/src/types/generated/CreateRoleRequest.ts +++ b/fe/src/types/generated/CreateRoleRequest.ts @@ -12,11 +12,11 @@ export type CreateRoleRequest = { /** * Optional description */ - description?: string + description: string | null /** * Whether this role grants super admin privileges */ - super_admin?: boolean + super_admin: boolean | null /** * Direct permissions for this role */ diff --git a/fe/src/types/generated/CreateUserRequest.ts b/fe/src/types/generated/CreateUserRequest.ts index 6792edb2..a4095368 100644 --- a/fe/src/types/generated/CreateUserRequest.ts +++ b/fe/src/types/generated/CreateUserRequest.ts @@ -27,13 +27,13 @@ export type CreateUserRequest = { /** * Role UUIDs to assign to this user (optional) */ - role_uuids?: string[] + role_uuids: string[] | null /** * Whether user is active */ - is_active?: boolean + is_active: boolean | null /** * Super admin flag */ - super_admin?: boolean + super_admin: boolean | null } diff --git a/fe/src/types/generated/EmailTemplateListQuery.ts b/fe/src/types/generated/EmailTemplateListQuery.ts index 5aed7536..4779fff2 100644 --- a/fe/src/types/generated/EmailTemplateListQuery.ts +++ b/fe/src/types/generated/EmailTemplateListQuery.ts @@ -8,5 +8,5 @@ export type EmailTemplateListQuery = { /** * Filter by template type: "system" or "workflow" */ - type?: EmailTemplateType + type: EmailTemplateType | null } diff --git a/fe/src/types/generated/PostRunSendEmail.ts b/fe/src/types/generated/PostRunSendEmail.ts index 84c52271..d86a1c27 100644 --- a/fe/src/types/generated/PostRunSendEmail.ts +++ b/fe/src/types/generated/PostRunSendEmail.ts @@ -17,7 +17,7 @@ export type PostRunSendEmail = { /** * Optional CC */ - cc?: Array + cc: Array | null /** * When to fire this action */ diff --git a/fe/src/types/generated/SendEmailTransform.ts b/fe/src/types/generated/SendEmailTransform.ts index b33e3ece..9d639d7d 100644 --- a/fe/src/types/generated/SendEmailTransform.ts +++ b/fe/src/types/generated/SendEmailTransform.ts @@ -16,7 +16,7 @@ export type SendEmailTransform = { /** * Optional CC recipients */ - cc?: Array + cc: Array | null /** * Normalized field to store send result (`"queued"`, `"mail_not_configured"`, or error) */ diff --git a/fe/src/types/generated/SystemLogQuery.ts b/fe/src/types/generated/SystemLogQuery.ts index fc5b1116..e0da329f 100644 --- a/fe/src/types/generated/SystemLogQuery.ts +++ b/fe/src/types/generated/SystemLogQuery.ts @@ -10,33 +10,33 @@ export type SystemLogQuery = { /** * Page number (1-based, default: 1) */ - page?: number + page: number | null /** * Items per page (default: 20, max: 100) */ - page_size?: number + page_size: number | null /** * Filter by log type */ - log_type?: SystemLogType + log_type: SystemLogType | null /** * Filter by resource type */ - resource_type?: SystemLogResourceType + resource_type: SystemLogResourceType | null /** * Filter by status */ - status?: SystemLogStatus + status: SystemLogStatus | null /** * Filter by resource UUID */ - resource_uuid?: string + resource_uuid: string | null /** * Filter logs created after this timestamp (ISO 8601) */ - date_from?: string + date_from: string | null /** * Filter logs created before this timestamp (ISO 8601) */ - date_to?: string + date_to: string | null } diff --git a/fe/src/types/generated/UpdateEmailTemplateRequest.ts b/fe/src/types/generated/UpdateEmailTemplateRequest.ts index 337adbdd..8aba1d3b 100644 --- a/fe/src/types/generated/UpdateEmailTemplateRequest.ts +++ b/fe/src/types/generated/UpdateEmailTemplateRequest.ts @@ -7,7 +7,7 @@ export type UpdateEmailTemplateRequest = { /** * New display name (only honoured for workflow templates) */ - name?: string + name: string | null /** * Updated subject line */ diff --git a/fe/src/types/generated/UpdateRoleRequest.ts b/fe/src/types/generated/UpdateRoleRequest.ts index 22e52f37..1071e607 100644 --- a/fe/src/types/generated/UpdateRoleRequest.ts +++ b/fe/src/types/generated/UpdateRoleRequest.ts @@ -12,11 +12,11 @@ export type UpdateRoleRequest = { /** * Optional description */ - description?: string + description: string | null /** * Whether this role grants super admin privileges */ - super_admin?: boolean + super_admin: boolean | null /** * Direct permissions for this role */ diff --git a/fe/src/types/generated/UpdateUserRequest.ts b/fe/src/types/generated/UpdateUserRequest.ts index 250e1c9d..868e4a2f 100644 --- a/fe/src/types/generated/UpdateUserRequest.ts +++ b/fe/src/types/generated/UpdateUserRequest.ts @@ -7,29 +7,29 @@ export type UpdateUserRequest = { /** * Email address (optional) */ - email?: string + email: string | null /** * Password (optional, only set if provided) */ - password?: string + password: string | null /** * First name (optional) */ - first_name?: string + first_name: string | null /** * Last name (optional) */ - last_name?: string + last_name: string | null /** * Role UUIDs to assign to this user (optional) */ - role_uuids?: string[] + role_uuids: string[] | null /** * Whether user is active (optional) */ - is_active?: boolean + is_active: boolean | null /** * Super admin flag (optional) */ - super_admin?: boolean + super_admin: boolean | null } diff --git a/fe/src/types/generated/ValidationViolation.ts b/fe/src/types/generated/ValidationViolation.ts index 4fd97b4d..1011abd4 100644 --- a/fe/src/types/generated/ValidationViolation.ts +++ b/fe/src/types/generated/ValidationViolation.ts @@ -15,5 +15,5 @@ export type ValidationViolation = { /** * Optional error code (e.g., `"NOT_BLANK"`, `"NOT_NULL"`) */ - code?: string + code: string | null } diff --git a/fe/src/types/schemas/api-key.ts b/fe/src/types/schemas/api-key.ts index c5df67d0..f4d85081 100644 --- a/fe/src/types/schemas/api-key.ts +++ b/fe/src/types/schemas/api-key.ts @@ -7,8 +7,8 @@ import { API_KEY_NAME_MIN_LENGTH } from '../generated/validation' // Create API key request schema (form validation) 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({ diff --git a/fe/src/types/schemas/base.ts b/fe/src/types/schemas/base.ts index 40b269d6..4c7b2de4 100644 --- a/fe/src/types/schemas/base.ts +++ b/fe/src/types/schemas/base.ts @@ -25,7 +25,7 @@ export const TimestampSchema = z.string().refine( 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({ diff --git a/fe/src/types/schemas/contract.test.ts b/fe/src/types/schemas/contract.test.ts index 4c2438aa..79e4b7ab 100644 --- a/fe/src/types/schemas/contract.test.ts +++ b/fe/src/types/schemas/contract.test.ts @@ -113,6 +113,9 @@ describe('Generated type contract tests', () => { password: 'securepassword123', first_name: 'New', last_name: 'User', + role_uuids: null, + is_active: null, + super_admin: null, }) expect(fixture.username).toBeTruthy() }) @@ -194,6 +197,7 @@ describe('Generated type contract tests', () => { it('CreateApiKeyRequest matches expected request shape', () => { const fixture = assertType({ name: 'Test Key', + description: null, expires_in_days: 365, }) expect(fixture.name).toBe('Test Key') @@ -317,6 +321,7 @@ describe('Generated type contract tests', () => { const fixture = assertType({ field: 'email', message: 'Invalid email format', + code: null, }) expect(fixture.field).toBe('email') }) diff --git a/fe/src/types/schemas/dsl.ts b/fe/src/types/schemas/dsl.ts index ed03b7ce..1fa16946 100644 --- a/fe/src/types/schemas/dsl.ts +++ b/fe/src/types/schemas/dsl.ts @@ -255,7 +255,7 @@ 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', [ diff --git a/fe/src/types/schemas/role.ts b/fe/src/types/schemas/role.ts index d25abd80..9ff568d7 100644 --- a/fe/src/types/schemas/role.ts +++ b/fe/src/types/schemas/role.ts @@ -49,15 +49,15 @@ export const PermissionSchema = z.object({ // Request schemas (form validation) — bind to generated types export const CreateRoleRequestSchema = z.object({ name: z.string(), - description: z.string().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().optional(), - super_admin: z.boolean().optional(), + description: z.string().nullable(), + super_admin: z.boolean().nullable(), permissions: z.array(PermissionSchema), }) satisfies z.ZodType diff --git a/fe/src/types/schemas/user.ts b/fe/src/types/schemas/user.ts index 3db9d866..7436bd2b 100644 --- a/fe/src/types/schemas/user.ts +++ b/fe/src/types/schemas/user.ts @@ -22,20 +22,20 @@ export const CreateUserRequestSchema = z.object({ 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 From 5dc6417df9473e4fd76145cc8820428352cb3f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 00:07:44 +0200 Subject: [PATCH 04/11] chore: fixing npm dependencies --- fe/package-lock.json | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/fe/package-lock.json b/fe/package-lock.json index 6c6fff03..fa481677 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -203,7 +203,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -226,11 +225,34 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "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.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", @@ -238,6 +260,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2440,7 +2463,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2484,7 +2506,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -2819,7 +2840,6 @@ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", @@ -3097,7 +3117,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3576,7 +3595,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3636,7 +3654,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5150,7 +5167,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5812,7 +5828,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5868,7 +5883,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5965,7 +5979,6 @@ "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.2.tgz", "integrity": "sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==", "devOptional": true, - "peer": true, "dependencies": { "@vuetify/loader-shared": "^2.1.1", "debug": "^4.3.3", @@ -5985,7 +5998,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -6063,7 +6075,6 @@ "version": "3.5.25", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", @@ -6202,7 +6213,6 @@ "version": "3.11.2", "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.2.tgz", "integrity": "sha512-1lL0qN6JIdbx6xGYpo6dnx378EfC0t4EotPJdP4go8ThmIdRO3xLva1ALxhxi5lSYTht4R9OVk9miVnwVfDx3A==", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/johnleider" From d6276758af2626ca136181f9841d20172393f268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 00:36:12 +0200 Subject: [PATCH 05/11] fix: adding prettier to the ci (so the generated ts-bindings can be compared) --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 266d7df5..6b397a60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,6 +304,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 +332,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 From 70932a4ad86b8dc7661f57affd20a5d1d9f4dcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 21:59:19 +0200 Subject: [PATCH 06/11] fix: more bindings --- crates/api/src/admin/auth/models.rs | 12 +++ crates/api/src/admin/auth/routes.rs | 12 ++- .../src/admin/entity_definitions/routes.rs | 20 ++-- crates/api/src/admin/meta/models.rs | 21 +++- crates/api/src/admin/system/models.rs | 46 +++++++-- crates/api/src/admin/workflows/models.rs | 15 +++ crates/api/src/admin/workflows/routes/runs.rs | 25 ++--- crates/api/src/query/mod.rs | 27 ++++-- crates/services/src/auth.rs | 21 +--- crates/workflow/src/data/requests.rs | 15 ++- fe/src/api/clients/api-keys.test.ts | 22 ++++- fe/src/api/clients/api-keys.ts | 17 ++-- fe/src/api/clients/auth.ts | 13 +-- fe/src/api/clients/dsl.ts | 5 +- fe/src/api/clients/entity-definitions.test.ts | 14 ++- fe/src/api/clients/entity-definitions.ts | 19 ++-- fe/src/api/clients/meta.ts | 35 ++----- fe/src/api/clients/permissions.test.ts | 14 ++- fe/src/api/clients/query.test.ts | 97 +++++++++++++++++++ fe/src/api/clients/query.ts | 23 +++++ fe/src/api/clients/roles.test.ts | 22 ++++- fe/src/api/clients/roles.ts | 17 ++-- fe/src/api/clients/system-logs.ts | 13 +-- fe/src/api/clients/system.test.ts | 17 +++- fe/src/api/clients/system.ts | 78 ++++++--------- fe/src/api/clients/users.test.ts | 22 ++++- fe/src/api/clients/users.ts | 17 ++-- fe/src/api/clients/workflows.test.ts | 80 ++++++++++++--- fe/src/api/clients/workflows.ts | 73 +++++--------- .../components/common/LicenseBanner.test.ts | 6 ++ fe/src/composables/useApiKeys.test.ts | 24 +++-- fe/src/composables/useApiKeys.ts | 18 ++-- fe/src/composables/useEntities.ts | 7 +- fe/src/composables/useEntityDefinitions.ts | 7 +- fe/src/composables/useErrorHandler.ts | 6 ++ fe/src/composables/useRoles.test.ts | 5 +- fe/src/composables/useRoles.ts | 5 +- fe/src/composables/useUsers.test.ts | 5 +- fe/src/composables/useUsers.ts | 5 +- fe/src/pages/api-keys/ApiKeysPage.vue | 8 +- fe/src/pages/entities/EntitiesPage.vue | 7 +- .../EntityDefinitionsPage.vue | 7 +- fe/src/pages/workflows/WorkflowsPage.vue | 30 +++--- fe/src/stores/auth.ts | 6 ++ fe/src/stores/license.test.ts | 6 ++ fe/src/stores/versions.test.ts | 4 +- fe/src/types/generated/ComponentVersionDto.ts | 19 ++++ .../types/generated/CreateWorkflowRequest.ts | 35 +++++++ fe/src/types/generated/DashboardStats.ts | 25 +++++ fe/src/types/generated/EntityFieldInfo.ts | 23 +++++ fe/src/types/generated/EntityStats.ts | 16 +++ fe/src/types/generated/EntityTypeCount.ts | 15 +++ .../generated/EntityVersioningSettingsDto.ts | 22 +++++ fe/src/types/generated/LicenseStateDto.ts | 6 ++ fe/src/types/generated/LicenseStatusDto.ts | 44 +++++++++ fe/src/types/generated/PaginationQuery.ts | 40 ++++++++ fe/src/types/generated/SortingQuery.ts | 15 +++ fe/src/types/generated/SystemVersionsDto.ts | 20 ++++ fe/src/types/generated/UpdateSettingsBody.ts | 19 ++++ .../types/generated/UpdateWorkflowRequest.ts | 35 +++++++ .../UpdateWorkflowRunLogSettingsBody.ts | 19 ++++ .../generated/UserPermissionsResponse.ts | 19 ++++ .../generated/WorkflowRunLogSettingsDto.ts | 19 ++++ .../generated/WorkflowRunUploadResponse.ts | 19 ++++ fe/src/types/generated/WorkflowStats.ts | 16 +++ .../generated/WorkflowWithLatestStatus.ts | 19 ++++ 66 files changed, 1089 insertions(+), 324 deletions(-) create mode 100644 fe/src/api/clients/query.test.ts create mode 100644 fe/src/api/clients/query.ts create mode 100644 fe/src/types/generated/ComponentVersionDto.ts create mode 100644 fe/src/types/generated/CreateWorkflowRequest.ts create mode 100644 fe/src/types/generated/DashboardStats.ts create mode 100644 fe/src/types/generated/EntityFieldInfo.ts create mode 100644 fe/src/types/generated/EntityStats.ts create mode 100644 fe/src/types/generated/EntityTypeCount.ts create mode 100644 fe/src/types/generated/EntityVersioningSettingsDto.ts create mode 100644 fe/src/types/generated/LicenseStateDto.ts create mode 100644 fe/src/types/generated/LicenseStatusDto.ts create mode 100644 fe/src/types/generated/PaginationQuery.ts create mode 100644 fe/src/types/generated/SortingQuery.ts create mode 100644 fe/src/types/generated/SystemVersionsDto.ts create mode 100644 fe/src/types/generated/UpdateSettingsBody.ts create mode 100644 fe/src/types/generated/UpdateWorkflowRequest.ts create mode 100644 fe/src/types/generated/UpdateWorkflowRunLogSettingsBody.ts create mode 100644 fe/src/types/generated/UserPermissionsResponse.ts create mode 100644 fe/src/types/generated/WorkflowRunLogSettingsDto.ts create mode 100644 fe/src/types/generated/WorkflowRunUploadResponse.ts create mode 100644 fe/src/types/generated/WorkflowStats.ts create mode 100644 fe/src/types/generated/WorkflowWithLatestStatus.ts diff --git a/crates/api/src/admin/auth/models.rs b/crates/api/src/admin/auth/models.rs index 8f86648e..e4edf137 100644 --- a/crates/api/src/admin/auth/models.rs +++ b/crates/api/src/admin/auth/models.rs @@ -11,6 +11,18 @@ use validator::Validate; #[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)] 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/entity_definitions/routes.rs b/crates/api/src/admin/entity_definitions/routes.rs index 1470fb68..2a732d7e 100644 --- a/crates/api/src/admin/entity_definitions/routes.rs +++ b/crates/api/src/admin/entity_definitions/routes.rs @@ -7,6 +7,7 @@ use log::{debug, error, info}; use r_data_core_core::permissions::role::{PermissionType, ResourceNamespace}; use serde::Serialize; use serde_json::json; +use ts_rs::TS; use time::OffsetDateTime; use uuid::Uuid; @@ -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/system/models.rs b/crates/api/src/admin/system/models.rs index 19830e0c..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 @@ -277,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, @@ -285,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/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/query/mod.rs b/crates/api/src/query/mod.rs index 70f74ebc..cc8ff3cf 100644 --- a/crates/api/src/query/mod.rs +++ b/crates/api/src/query/mod.rs @@ -1,6 +1,7 @@ -use serde::Deserialize; +use serde::{Deserialize, 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 @@ -55,26 +56,35 @@ 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, Deserialize, 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)] + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_i64")] + #[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)] + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_i64")] + #[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)] + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_i64")] + #[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)] + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_i64")] + #[ts(type = "number | null")] pub offset: Option, } @@ -236,11 +246,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/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/fe/src/api/clients/api-keys.test.ts b/fe/src/api/clients/api-keys.test.ts index 26822afc..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 diff --git a/fe/src/api/clients/api-keys.ts b/fe/src/api/clients/api-keys.ts index 4b34d8ed..c7b359fc 100644 --- a/fe/src/api/clients/api-keys.ts +++ b/fe/src/api/clients/api-keys.ts @@ -1,20 +1,19 @@ import type { ApiKeyResponse } from '@/types/generated/ApiKeyResponse' import type { ApiKeyCreatedResponse } from '@/types/generated/ApiKeyCreatedResponse' +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 + pagination: PaginationQuery, + sorting?: SortingQuery | null ): Promise<{ data: ApiKeyResponse[]; meta?: ResponseMeta }> { - 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) + 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/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/entity-definitions.test.ts b/fe/src/api/clients/entity-definitions.test.ts index 2ec22f96..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) diff --git a/fe/src/api/clients/entity-definitions.ts b/fe/src/api/clients/entity-definitions.ts index 76356827..5c7d9a85 100644 --- a/fe/src/api/clients/entity-definitions.ts +++ b/fe/src/api/clients/entity-definitions.ts @@ -7,16 +7,17 @@ import type { 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 + pagination: PaginationQuery ): Promise<{ data: EntityDefinition[]; meta?: ResponseMeta }> { - const pageSize = limit ?? this.getDefaultPageSize() return this.paginatedRequest( - `/admin/api/v1/entity-definitions?limit=${pageSize}&offset=${offset}` + `/admin/api/v1/entity-definitions${buildListQueryString(pagination)}` ) } @@ -56,12 +57,10 @@ export class EntityDefinitionsClient extends BaseTypedHttpClient { }) } - 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 { 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 059372f2..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) 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 a41ee15d..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 diff --git a/fe/src/api/clients/roles.ts b/fe/src/api/clients/roles.ts index 447d6ad4..e4fa9960 100644 --- a/fe/src/api/clients/roles.ts +++ b/fe/src/api/clients/roles.ts @@ -1,4 +1,6 @@ import type { RoleResponse } from '@/types/generated/RoleResponse' +import type { PaginationQuery } from '@/types/generated/PaginationQuery' +import type { SortingQuery } from '@/types/generated/SortingQuery' import type { CreateRoleRequest, UpdateRoleRequest, @@ -6,19 +8,16 @@ import type { 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 + pagination: PaginationQuery, + sorting?: SortingQuery | null ): Promise<{ data: RoleResponse[]; meta?: ResponseMeta }> { - 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) + 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 b4177822..57d9ed2c 100644 --- a/fe/src/api/clients/system-logs.ts +++ b/fe/src/api/clients/system-logs.ts @@ -11,18 +11,7 @@ export interface SystemLogListResult { } export class SystemLogClient extends BaseTypedHttpClient { - async list( - params: SystemLogQuery = { - page: null, - page_size: null, - log_type: null, - resource_type: null, - status: null, - resource_uuid: null, - date_from: null, - date_to: null, - } - ): 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 73aeb73e..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 diff --git a/fe/src/api/clients/users.ts b/fe/src/api/clients/users.ts index 9410e668..11578171 100644 --- a/fe/src/api/clients/users.ts +++ b/fe/src/api/clients/users.ts @@ -1,19 +1,18 @@ import type { UserResponse } from '@/types/generated/UserResponse' +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 + pagination: PaginationQuery, + sorting?: SortingQuery | null ): Promise<{ data: UserResponse[]; meta?: ResponseMeta }> { - 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) + 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 b2143270..8b160637 100644 --- a/fe/src/api/clients/workflows.ts +++ b/fe/src/api/clients/workflows.ts @@ -2,10 +2,17 @@ import type { WorkflowDetail } from '@/types/generated/WorkflowDetail' import type { WorkflowSummary } from '@/types/generated/WorkflowSummary' import type { WorkflowRunLogDto } from '@/types/generated/WorkflowRunLogDto' 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 { DslOptionsResponse, WorkflowRun, WorkflowConfig, ResponseMeta } from '@/types/schemas' +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' @@ -15,49 +22,26 @@ export class WorkflowsClient extends BaseTypedHttpClient { } async getWorkflows( - page = 1, - itemsPerPage = 20, - sortBy?: string | null, - sortOrder?: 'asc' | 'desc' | null + pagination: PaginationQuery, + sorting?: SortingQuery | null ): Promise<{ data: WorkflowSummary[]; meta?: ResponseMeta }> { - 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) + 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 { + 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), @@ -84,37 +68,31 @@ export class WorkflowsClient extends BaseTypedHttpClient { async getWorkflowRuns( workflowUuid: string, - page = 1, - perPage = 20 + 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 + 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 + 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 @@ -127,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) { @@ -141,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) { @@ -163,8 +140,8 @@ 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 }), }) 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/composables/useApiKeys.test.ts b/fe/src/composables/useApiKeys.test.ts index 8cf88bb0..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 () => { @@ -228,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, + }) }) }) @@ -247,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.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.ts b/fe/src/composables/useErrorHandler.ts index ccf89afd..df4ff2b3 100644 --- a/fe/src/composables/useErrorHandler.ts +++ b/fe/src/composables/useErrorHandler.ts @@ -3,6 +3,12 @@ import { useTranslations } from './useTranslations' import { ValidationError } from '@/api/typed-client' import { HttpError } from '@/api/errors' +/** + * 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 10621ad1..795717e9 100644 --- a/fe/src/composables/useRoles.test.ts +++ b/fe/src/composables/useRoles.test.ts @@ -73,7 +73,10 @@ describe('useRoles', () => { 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) }) 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 4d5f2039..56a82873 100644 --- a/fe/src/composables/useUsers.test.ts +++ b/fe/src/composables/useUsers.test.ts @@ -79,7 +79,10 @@ describe('useUsers', () => { 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) }) 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 e640a4f7..3087f9e0 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -9,6 +9,12 @@ import { useVersionStore } from './versions' import { useCapabilitiesStore } from './capabilities' 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 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/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/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/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/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/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/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/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/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/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/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/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/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/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 +} From 2f1345adcb7406318ebb6b3e4d0cab85679370ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 21:59:52 +0200 Subject: [PATCH 07/11] chore: fmt --- crates/api/src/admin/entity_definitions/routes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/api/src/admin/entity_definitions/routes.rs b/crates/api/src/admin/entity_definitions/routes.rs index 2a732d7e..11b1420b 100644 --- a/crates/api/src/admin/entity_definitions/routes.rs +++ b/crates/api/src/admin/entity_definitions/routes.rs @@ -7,8 +7,8 @@ use log::{debug, error, info}; use r_data_core_core::permissions::role::{PermissionType, ResourceNamespace}; use serde::Serialize; use serde_json::json; -use ts_rs::TS; use time::OffsetDateTime; +use ts_rs::TS; use uuid::Uuid; use crate::admin::entity_definitions::conversions::entity_definition_to_schema_model; From ebf959e1284d7ee970c5ec8e4ec572e4573140ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 22:09:09 +0200 Subject: [PATCH 08/11] chore: removing deserialisation issues --- crates/api/src/query/mod.rs | 70 ++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/crates/api/src/query/mod.rs b/crates/api/src/query/mod.rs index cc8ff3cf..5528f3d2 100644 --- a/crates/api/src/query/mod.rs +++ b/crates/api/src/query/mod.rs @@ -1,25 +1,30 @@ -use serde::{Deserialize, Serialize}; +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 @@ -56,38 +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, Serialize, Deserialize, ToSchema, TS)] +#[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(default)] - #[serde(deserialize_with = "deserialize_optional_i64")] #[ts(type = "number | null")] pub page: Option, /// Items per page - defaults to 20, max 100 /// Use with `page` for page-based pagination - #[serde(default)] - #[serde(deserialize_with = "deserialize_optional_i64")] #[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(default)] - #[serde(deserialize_with = "deserialize_optional_i64")] #[ts(type = "number | null")] pub limit: Option, /// Offset (alternative to page) - defaults to 0 /// Use with `limit` for offset-based pagination - #[serde(default)] - #[serde(deserialize_with = "deserialize_optional_i64")] #[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 /// From 0617ddd1e6ab15d05679f6fb430aaeea59813dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 22:09:36 +0200 Subject: [PATCH 09/11] chore: fmt --- crates/api/src/query/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/api/src/query/mod.rs b/crates/api/src/query/mod.rs index 5528f3d2..1d94b73f 100644 --- a/crates/api/src/query/mod.rs +++ b/crates/api/src/query/mod.rs @@ -20,9 +20,9 @@ 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")) - }), + Self::Str(s) => s + .parse::() + .map_err(|_| E::custom(format!("invalid type: string \"{s}\", expected i64"))), } } } From dd0ecc317efc9a4c5102c9289790c8066dd9fa27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 22:29:35 +0200 Subject: [PATCH 10/11] ci: making sure branch names and commits (cnoventional) are respected in CI --- .githooks/pre-push | 37 ++++++++++++++- .github/workflows/ci.yml | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 10fc9eee..c86dc2c6 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -34,6 +34,8 @@ 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)" "$*" } @@ -134,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_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_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_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}" @@ -212,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" @@ -484,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 6b397a60..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 # ============================================================================ @@ -676,6 +753,8 @@ jobs: ci-gate: needs: - changes + - branch-name + - commit-lint - backend-clippy - backend-format - backend-audit @@ -690,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: | From 6d03f93cd81f23047250191e50b504532ecab6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bent=20Br=C3=BCggemann?= Date: Tue, 21 Apr 2026 22:44:37 +0200 Subject: [PATCH 11/11] fix: trying to fix eslint issues of the projectService --- fe/eslint.config.js | 8 ++++---- fe/tsconfig.json | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) 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/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" - } ] }