spine-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module schema-utils
|
|
3
|
+
* @audience both
|
|
4
|
+
* @layer shared-core
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Schema generation and field-level data transformation for all Spine types.
|
|
8
|
+
*
|
|
9
|
+
* Three public functions form the core contract:
|
|
10
|
+
* - `generateValidationSchema` — derives a runtime validation schema from a
|
|
11
|
+
* `design_schema`, stripping display/permission info and keeping only
|
|
12
|
+
* structural constraints.
|
|
13
|
+
* - `sanitizeFieldData` — coerces and validates a single field value for
|
|
14
|
+
* write (create/update) operations. Throws on invalid data.
|
|
15
|
+
* - `formatFieldData` — converts a stored field value to a human-readable
|
|
16
|
+
* display string for read operations. Never throws.
|
|
17
|
+
* - `transformRecordData` — applies sanitize or format to all fields in a
|
|
18
|
+
* record using a pre-generated ValidationSchema.
|
|
19
|
+
*
|
|
20
|
+
* These are called by `permissions.ts` (`validateFirstSurfaceUpdatePermissions`
|
|
21
|
+
* and `sanitizeFirstSurfaceRecordData`) — do not call them directly from API
|
|
22
|
+
* handlers. Use `PermissionEngine.sanitizeRecordData` / `validateUpdatePermissions`.
|
|
23
|
+
*
|
|
24
|
+
* INVARIANT: all sanitize functions throw on invalid data. Callers must catch
|
|
25
|
+
* errors and convert them to field-level validation error messages.
|
|
26
|
+
* INVARIANT: all format functions return the raw data unchanged if formatting
|
|
27
|
+
* is not applicable (never throw, never return null for non-null input).
|
|
28
|
+
*
|
|
29
|
+
* @seeAlso permissions.ts (primary caller of sanitizeFieldData, formatFieldData)
|
|
30
|
+
* @seeAlso src/types/types.ts (FieldDefinition interface)
|
|
31
|
+
* @seeAlso index.ts (not re-exported — internal to core; use PermissionEngine)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { FieldDefinition } from '../../src/types/types'
|
|
35
|
+
|
|
36
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Structural-only validation schema derived from a `design_schema`.
|
|
40
|
+
*
|
|
41
|
+
* Contains one entry per field with the field's `data_type` and any explicit
|
|
42
|
+
* `validation` constraints. Display properties (`display_type`, views, sections)
|
|
43
|
+
* and permission properties are stripped. Used as input to `sanitizeFieldData`
|
|
44
|
+
* and `formatFieldData`.
|
|
45
|
+
*
|
|
46
|
+
* @inputSpec none — output type of generateValidationSchema
|
|
47
|
+
* @outputSpec fields: Record<fieldName, { data_type, required?, ...constraints }>
|
|
48
|
+
* @calledBy generateValidationSchema (producer), transformRecordData,
|
|
49
|
+
* permissions.ts validateFirstSurfaceUpdatePermissions (consumer)
|
|
50
|
+
*/
|
|
51
|
+
export interface ValidationSchema {
|
|
52
|
+
fields: Record<string, {
|
|
53
|
+
data_type: string
|
|
54
|
+
required?: boolean
|
|
55
|
+
[key: string]: any // Type-specific validation properties
|
|
56
|
+
}>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── PUBLIC API ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derives a `ValidationSchema` from a `design_schema` by extracting only
|
|
63
|
+
* structural constraints (data_type, required, validation.*) and discarding
|
|
64
|
+
* display, permission, and view configuration.
|
|
65
|
+
*
|
|
66
|
+
* The resulting schema is used to drive `sanitizeFieldData` and
|
|
67
|
+
* `formatFieldData` for every field in a record. It is generated once per
|
|
68
|
+
* type and passed to `transformRecordData` for bulk field processing.
|
|
69
|
+
*
|
|
70
|
+
* @param designSchema - The full design_schema object from a type record
|
|
71
|
+
* @returns ValidationSchema with one entry per field
|
|
72
|
+
* @throws never — returns empty schema if designSchema.fields is missing
|
|
73
|
+
* @inputSpec designSchema.fields: Record<fieldName, FieldDefinition> — must match
|
|
74
|
+
* the FieldDefinition interface from src/types/types.ts
|
|
75
|
+
* @inputSpec designSchema.fields[x].data_type: string — required in every field
|
|
76
|
+
* @outputSpec ValidationSchema.fields: Record<string, { data_type, required, ...constraints }>
|
|
77
|
+
* @sideEffects none
|
|
78
|
+
* @calledBy permissions.ts (validateFirstSurfaceUpdatePermissions), any caller
|
|
79
|
+
* needing a validation schema without the full design_schema overhead
|
|
80
|
+
* @testUnit tests/unit/schema-utils.test.ts — 'generateValidationSchema' describe block
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const schema = generateValidationSchema(record.design_schema)
|
|
85
|
+
* const cleaned = transformRecordData(record.data, schema, 'sanitize')
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function generateValidationSchema(designSchema: any): ValidationSchema {
|
|
89
|
+
const validationSchema: ValidationSchema = {
|
|
90
|
+
fields: {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!designSchema.fields) {
|
|
94
|
+
return validationSchema
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [fieldName, fieldDef] of Object.entries(designSchema.fields)) {
|
|
98
|
+
const field = fieldDef as FieldDefinition
|
|
99
|
+
|
|
100
|
+
// Extract only structural validation properties exactly as declared
|
|
101
|
+
const validationField: any = {
|
|
102
|
+
data_type: field.data_type,
|
|
103
|
+
required: field.required
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add explicit validation constraints exactly as declared
|
|
107
|
+
if (field.validation) {
|
|
108
|
+
Object.assign(validationField, field.validation)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add type-specific constraint properties (moved out of validation for clarity)
|
|
112
|
+
if (field.options) {
|
|
113
|
+
validationField.options = field.options
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add reference properties if they exist
|
|
117
|
+
if (field.data_type === 'reference' && field.validation) {
|
|
118
|
+
if (field.validation.reference_kind) validationField.reference_kind = field.validation.reference_kind
|
|
119
|
+
if (field.validation.reference_type) validationField.reference_type = field.validation.reference_type
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
validationSchema.fields[fieldName] = validationField
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return validationSchema
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Coerces and validates a single field value for a write (create/update) operation.
|
|
130
|
+
*
|
|
131
|
+
* Dispatches to a type-specific sanitizer based on `data_type`. Every sanitizer:
|
|
132
|
+
* - Coerces the input to the correct type
|
|
133
|
+
* - Applies constraint validation (min/max/length/pattern/options)
|
|
134
|
+
* - Throws a descriptive `Error` on the first validation failure
|
|
135
|
+
* - Returns the cleaned value on success
|
|
136
|
+
*
|
|
137
|
+
* Unknown `data_type` values pass through unchanged (no throw).
|
|
138
|
+
*
|
|
139
|
+
* @param data - Raw field value from the request body
|
|
140
|
+
* @param data_type - The field's declared data_type from design_schema.fields[x]
|
|
141
|
+
* @param validation - Optional validation constraints from the field definition
|
|
142
|
+
* @returns Sanitized value in the correct type for storage
|
|
143
|
+
* @throws Error — descriptive message naming the field constraint violated
|
|
144
|
+
* @inputSpec data: any — null and undefined are returned as-is without sanitization
|
|
145
|
+
* @inputSpec data_type: string — one of the 21 supported type keys (see switch below)
|
|
146
|
+
* @inputSpec validation: object | undefined — type-specific constraints
|
|
147
|
+
* @outputSpec any — coerced value matching the data_type storage format
|
|
148
|
+
* @sideEffects none
|
|
149
|
+
* @calledBy permissions.ts (validateFirstSurfaceUpdatePermissions, per-field loop)
|
|
150
|
+
* @calls sanitizeText | sanitizeTextarea | sanitizeEmail | sanitizeNumber | etc.
|
|
151
|
+
* @testUnit tests/unit/schema-utils.test.ts — 'sanitizeFieldData' describe block
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* const clean = sanitizeFieldData('hello@EXAMPLE.COM', 'email')
|
|
156
|
+
* // → 'hello@example.com'
|
|
157
|
+
*
|
|
158
|
+
* sanitizeFieldData('not-a-url', 'url')
|
|
159
|
+
* // throws Error('Invalid URL format')
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function sanitizeFieldData(
|
|
163
|
+
data: any,
|
|
164
|
+
data_type: string,
|
|
165
|
+
validation?: any
|
|
166
|
+
): any {
|
|
167
|
+
if (data === null || data === undefined) {
|
|
168
|
+
return data
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
switch (data_type) {
|
|
172
|
+
case 'text':
|
|
173
|
+
return sanitizeText(data, validation)
|
|
174
|
+
case 'textarea':
|
|
175
|
+
return sanitizeTextarea(data, validation)
|
|
176
|
+
case 'rich_text':
|
|
177
|
+
return sanitizeRichText(data, validation)
|
|
178
|
+
case 'email':
|
|
179
|
+
return sanitizeEmail(data, validation)
|
|
180
|
+
case 'phone':
|
|
181
|
+
return sanitizePhone(data, validation)
|
|
182
|
+
case 'url':
|
|
183
|
+
return sanitizeUrl(data, validation)
|
|
184
|
+
case 'number':
|
|
185
|
+
return sanitizeNumber(data, validation)
|
|
186
|
+
case 'currency':
|
|
187
|
+
return sanitizeCurrency(data, validation)
|
|
188
|
+
case 'range':
|
|
189
|
+
return sanitizeRange(data, validation)
|
|
190
|
+
case 'date':
|
|
191
|
+
return sanitizeDate(data, validation)
|
|
192
|
+
case 'datetime':
|
|
193
|
+
return sanitizeDatetime(data, validation)
|
|
194
|
+
case 'boolean':
|
|
195
|
+
return sanitizeBoolean(data, validation)
|
|
196
|
+
case 'checkbox':
|
|
197
|
+
return sanitizeCheckbox(data, validation)
|
|
198
|
+
case 'select':
|
|
199
|
+
return sanitizeSelect(data, validation)
|
|
200
|
+
case 'multiselect':
|
|
201
|
+
return sanitizeMultiselect(data, validation)
|
|
202
|
+
case 'radio':
|
|
203
|
+
return sanitizeRadio(data, validation)
|
|
204
|
+
case 'color':
|
|
205
|
+
return sanitizeColor(data, validation)
|
|
206
|
+
case 'file':
|
|
207
|
+
return sanitizeFile(data, validation)
|
|
208
|
+
case 'image':
|
|
209
|
+
return sanitizeImage(data, validation)
|
|
210
|
+
case 'json':
|
|
211
|
+
return sanitizeJson(data, validation)
|
|
212
|
+
case 'reference':
|
|
213
|
+
return sanitizeReference(data, validation)
|
|
214
|
+
case 'address':
|
|
215
|
+
return sanitizeAddress(data, validation)
|
|
216
|
+
default:
|
|
217
|
+
return data
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Converts a stored field value to a human-readable display string.
|
|
223
|
+
*
|
|
224
|
+
* Dispatches to a type-specific formatter based on `data_type`. Formatters
|
|
225
|
+
* never throw — if the data cannot be formatted, the raw value is returned.
|
|
226
|
+
* Only types with meaningful display transformations have a case; all others
|
|
227
|
+
* fall through to the default (return data unchanged).
|
|
228
|
+
*
|
|
229
|
+
* @param data - Stored field value (from DB, post-sanitization)
|
|
230
|
+
* @param data_type - The field's declared data_type
|
|
231
|
+
* @param context - Optional context for type-specific formatting (e.g.
|
|
232
|
+
* `context.currency_code` for currency fields, `context.field` for boolean
|
|
233
|
+
* contextual labels like 'Active'/'Inactive')
|
|
234
|
+
* @returns Formatted display value
|
|
235
|
+
* @throws never
|
|
236
|
+
* @inputSpec data: any — null and undefined are returned as-is
|
|
237
|
+
* @inputSpec data_type: string — one of 21 supported keys; unknown → pass-through
|
|
238
|
+
* @inputSpec context: object | undefined — type-specific display hints
|
|
239
|
+
* @outputSpec any — display-ready value; string for most types, raw data for pass-through
|
|
240
|
+
* @sideEffects none
|
|
241
|
+
* @calledBy permissions.ts (sanitizeFirstSurfaceRecordData, per-field loop)
|
|
242
|
+
* @calls formatJson | formatDate | formatDatetime | formatCurrency | etc.
|
|
243
|
+
* @testUnit tests/unit/schema-utils.test.ts — 'formatFieldData' describe block
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* formatFieldData('2024-01-15', 'date')
|
|
248
|
+
* // → 'January 15, 2024'
|
|
249
|
+
*
|
|
250
|
+
* formatFieldData(1234.5, 'currency', { currency_code: 'USD' })
|
|
251
|
+
* // → '$1,234.50'
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export function formatFieldData(
|
|
255
|
+
data: any,
|
|
256
|
+
data_type: string,
|
|
257
|
+
context?: any
|
|
258
|
+
): any {
|
|
259
|
+
if (data === null || data === undefined) {
|
|
260
|
+
return data
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
switch (data_type) {
|
|
264
|
+
case 'json':
|
|
265
|
+
return formatJson(data)
|
|
266
|
+
case 'date':
|
|
267
|
+
return formatDate(data)
|
|
268
|
+
case 'datetime':
|
|
269
|
+
return formatDatetime(data)
|
|
270
|
+
case 'currency':
|
|
271
|
+
return formatCurrency(data, context)
|
|
272
|
+
case 'phone':
|
|
273
|
+
return formatPhone(data)
|
|
274
|
+
case 'url':
|
|
275
|
+
return formatUrl(data)
|
|
276
|
+
case 'reference':
|
|
277
|
+
return formatReference(data, context)
|
|
278
|
+
case 'address':
|
|
279
|
+
return formatAddress(data)
|
|
280
|
+
case 'multiselect':
|
|
281
|
+
return formatMultiselect(data)
|
|
282
|
+
case 'boolean':
|
|
283
|
+
return formatBoolean(data, context)
|
|
284
|
+
default:
|
|
285
|
+
return data
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Applies `sanitizeFieldData` or `formatFieldData` to all fields in a record,
|
|
291
|
+
* using the ValidationSchema for per-field type and constraint information.
|
|
292
|
+
*
|
|
293
|
+
* Fields not present in the schema are passed through unchanged. This is
|
|
294
|
+
* intentional — unknown fields are not rejected here; the PermissionEngine
|
|
295
|
+
* handles field-level access control separately.
|
|
296
|
+
*
|
|
297
|
+
* @param data - Key/value record of field names to raw or stored values
|
|
298
|
+
* @param validationSchema - Schema from `generateValidationSchema`
|
|
299
|
+
* @param operation - 'sanitize' for write path; 'format' for display path
|
|
300
|
+
* @param context - Optional context passed through to formatFieldData
|
|
301
|
+
* @returns Transformed record with the same keys
|
|
302
|
+
* @throws Error (sanitize mode only) — if any field fails validation
|
|
303
|
+
* @inputSpec data: Record<string, any> — flat field map
|
|
304
|
+
* @inputSpec validationSchema: ValidationSchema — from generateValidationSchema
|
|
305
|
+
* @inputSpec operation: 'sanitize' | 'format'
|
|
306
|
+
* @outputSpec Record<string, any> — same keys, transformed values
|
|
307
|
+
* @sideEffects none
|
|
308
|
+
* @calledBy Custom code in v2-custom/ that needs bulk field transformation
|
|
309
|
+
* @calls sanitizeFieldData | formatFieldData (per field)
|
|
310
|
+
* @testUnit tests/unit/schema-utils.test.ts — 'transformRecordData' describe block
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* const schema = generateValidationSchema(type.design_schema)
|
|
315
|
+
* const sanitized = transformRecordData(body.data, schema, 'sanitize')
|
|
316
|
+
* const formatted = transformRecordData(record.data, schema, 'format', ctx)
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
export function transformRecordData(
|
|
320
|
+
data: Record<string, any>,
|
|
321
|
+
validationSchema: ValidationSchema,
|
|
322
|
+
operation: 'sanitize' | 'format',
|
|
323
|
+
context?: any
|
|
324
|
+
): Record<string, any> {
|
|
325
|
+
const transformed: Record<string, any> = {}
|
|
326
|
+
|
|
327
|
+
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
328
|
+
const fieldValidation = validationSchema.fields[fieldName]
|
|
329
|
+
|
|
330
|
+
if (!fieldValidation) {
|
|
331
|
+
// No validation schema for this field, pass through as-is
|
|
332
|
+
transformed[fieldName] = fieldValue
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (operation === 'sanitize') {
|
|
337
|
+
transformed[fieldName] = sanitizeFieldData(
|
|
338
|
+
fieldValue,
|
|
339
|
+
fieldValidation.data_type,
|
|
340
|
+
fieldValidation
|
|
341
|
+
)
|
|
342
|
+
} else if (operation === 'format') {
|
|
343
|
+
transformed[fieldName] = formatFieldData(
|
|
344
|
+
fieldValue,
|
|
345
|
+
fieldValidation.data_type,
|
|
346
|
+
context
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return transformed
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ─── SANITIZE HELPERS ────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Trims, strips control characters, HTML-escapes, and applies minLength/
|
|
358
|
+
* maxLength/pattern constraints. Throws on minLength/pattern violation;
|
|
359
|
+
* silently truncates on maxLength.
|
|
360
|
+
* @throws Error on minLength or pattern violation
|
|
361
|
+
*/
|
|
362
|
+
function sanitizeText(data: any, validation?: any): string {
|
|
363
|
+
let text = String(data).trim()
|
|
364
|
+
|
|
365
|
+
// Remove control characters except newlines and tabs
|
|
366
|
+
text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
367
|
+
|
|
368
|
+
// Escape HTML entities
|
|
369
|
+
text = text
|
|
370
|
+
.replace(/&/g, '&')
|
|
371
|
+
.replace(/</g, '<')
|
|
372
|
+
.replace(/>/g, '>')
|
|
373
|
+
.replace(/"/g, '"')
|
|
374
|
+
.replace(/'/g, ''')
|
|
375
|
+
|
|
376
|
+
// Apply length constraints
|
|
377
|
+
if (validation?.minLength && text.length < validation.minLength) {
|
|
378
|
+
throw new Error(`Text must be at least ${validation.minLength} characters`)
|
|
379
|
+
}
|
|
380
|
+
if (validation?.maxLength && text.length > validation.maxLength) {
|
|
381
|
+
text = text.substring(0, validation.maxLength)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Apply pattern validation
|
|
385
|
+
if (validation?.pattern) {
|
|
386
|
+
const regex = new RegExp(validation.pattern)
|
|
387
|
+
if (!regex.test(text)) {
|
|
388
|
+
throw new Error(`Text does not match required pattern`)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return text
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Same as sanitizeText but preserves newlines. Strips control chars,
|
|
397
|
+
* HTML-escapes, applies minLength/maxLength.
|
|
398
|
+
* @throws Error on minLength violation
|
|
399
|
+
*/
|
|
400
|
+
function sanitizeTextarea(data: any, validation?: any): string {
|
|
401
|
+
let text = String(data).trim()
|
|
402
|
+
|
|
403
|
+
// Remove control characters except newlines and tabs
|
|
404
|
+
text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
405
|
+
|
|
406
|
+
// Escape HTML entities but preserve line breaks
|
|
407
|
+
text = text
|
|
408
|
+
.replace(/&/g, '&')
|
|
409
|
+
.replace(/</g, '<')
|
|
410
|
+
.replace(/>/g, '>')
|
|
411
|
+
.replace(/"/g, '"')
|
|
412
|
+
.replace(/'/g, ''')
|
|
413
|
+
|
|
414
|
+
// Apply length constraints
|
|
415
|
+
if (validation?.minLength && text.length < validation.minLength) {
|
|
416
|
+
throw new Error(`Text must be at least ${validation.minLength} characters`)
|
|
417
|
+
}
|
|
418
|
+
if (validation?.maxLength && text.length > validation.maxLength) {
|
|
419
|
+
text = text.substring(0, validation.maxLength)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return text
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Allowlist-based HTML sanitizer for rich text. Strips all tags not in the
|
|
427
|
+
* allowed set (`p br strong em u ol ul li a h1-h6`), removes script tags and
|
|
428
|
+
* `on*` event attributes, applies minLength/maxLength.
|
|
429
|
+
* @throws Error on minLength violation
|
|
430
|
+
*/
|
|
431
|
+
function sanitizeRichText(data: any, validation?: any): string {
|
|
432
|
+
let html = String(data).trim()
|
|
433
|
+
|
|
434
|
+
// Basic HTML sanitization - allow only safe tags
|
|
435
|
+
const allowedTags = ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
|
436
|
+
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g
|
|
437
|
+
|
|
438
|
+
html = html.replace(tagRegex, (match, tagName) => {
|
|
439
|
+
if (allowedTags.includes(tagName.toLowerCase())) {
|
|
440
|
+
return match
|
|
441
|
+
}
|
|
442
|
+
return ''
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// Remove script tags and on* attributes
|
|
446
|
+
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
447
|
+
html = html.replace(/on\w+\s*=/gi, '')
|
|
448
|
+
|
|
449
|
+
// Apply length constraints
|
|
450
|
+
if (validation?.minLength && html.length < validation.minLength) {
|
|
451
|
+
throw new Error(`Content must be at least ${validation.minLength} characters`)
|
|
452
|
+
}
|
|
453
|
+
if (validation?.maxLength && html.length > validation.maxLength) {
|
|
454
|
+
html = html.substring(0, validation.maxLength)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return html
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Lowercases, trims, and validates basic `name@domain.tld` format.
|
|
462
|
+
* @throws Error('Invalid email format') on invalid input
|
|
463
|
+
*/
|
|
464
|
+
function sanitizeEmail(data: any, validation?: any): string {
|
|
465
|
+
let email = String(data).toLowerCase().trim()
|
|
466
|
+
|
|
467
|
+
// Basic email validation
|
|
468
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
469
|
+
if (!emailRegex.test(email)) {
|
|
470
|
+
throw new Error('Invalid email format')
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return email
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Strips all non-digit/non-`+` characters. Applies optional pattern validation.
|
|
478
|
+
* @throws Error on pattern mismatch
|
|
479
|
+
*/
|
|
480
|
+
function sanitizePhone(data: any, validation?: any): string {
|
|
481
|
+
let phone = String(data).trim()
|
|
482
|
+
|
|
483
|
+
// Remove all non-digit characters except +
|
|
484
|
+
phone = phone.replace(/[^\d+]/g, '')
|
|
485
|
+
|
|
486
|
+
// Apply pattern validation if specified
|
|
487
|
+
if (validation?.pattern) {
|
|
488
|
+
const regex = new RegExp(validation.pattern)
|
|
489
|
+
if (!regex.test(phone)) {
|
|
490
|
+
throw new Error('Phone number does not match required format')
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return phone
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Parses via `new URL()` and validates `http:` or `https:` protocol.
|
|
499
|
+
* @throws Error on invalid URL or disallowed protocol
|
|
500
|
+
*/
|
|
501
|
+
function sanitizeUrl(data: any, validation?: any): string {
|
|
502
|
+
let url = String(data).trim()
|
|
503
|
+
|
|
504
|
+
// Basic URL validation
|
|
505
|
+
try {
|
|
506
|
+
const urlObj = new URL(url)
|
|
507
|
+
// Only allow http/https protocols
|
|
508
|
+
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
|
509
|
+
throw new Error('Only HTTP and HTTPS URLs are allowed')
|
|
510
|
+
}
|
|
511
|
+
return urlObj.toString()
|
|
512
|
+
} catch {
|
|
513
|
+
throw new Error('Invalid URL format')
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Coerces to Number, applies min/max constraints, rounds down to nearest
|
|
519
|
+
* step if `validation.step` is set.
|
|
520
|
+
* @throws Error on NaN or out-of-range
|
|
521
|
+
*/
|
|
522
|
+
function sanitizeNumber(data: any, validation?: any): number {
|
|
523
|
+
let num = Number(data)
|
|
524
|
+
|
|
525
|
+
if (isNaN(num)) {
|
|
526
|
+
throw new Error('Invalid number')
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Apply min/max constraints
|
|
530
|
+
if (validation?.min !== undefined && num < validation.min) {
|
|
531
|
+
throw new Error(`Number must be at least ${validation.min}`)
|
|
532
|
+
}
|
|
533
|
+
if (validation?.max !== undefined && num > validation.max) {
|
|
534
|
+
throw new Error(`Number must be at most ${validation.max}`)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Apply step constraint
|
|
538
|
+
if (validation?.step) {
|
|
539
|
+
const remainder = num % validation.step
|
|
540
|
+
if (remainder !== 0) {
|
|
541
|
+
num = num - remainder // Round down to nearest step
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return num
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Coerces to Number, rounds to 2 decimal places, applies min/max.
|
|
550
|
+
* @throws Error on NaN or out-of-range
|
|
551
|
+
*/
|
|
552
|
+
function sanitizeCurrency(data: any, validation?: any): number {
|
|
553
|
+
let num = Number(data)
|
|
554
|
+
|
|
555
|
+
if (isNaN(num)) {
|
|
556
|
+
throw new Error('Invalid currency amount')
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Round to 2 decimal places for currency
|
|
560
|
+
num = Math.round(num * 100) / 100
|
|
561
|
+
|
|
562
|
+
// Apply min/max constraints
|
|
563
|
+
if (validation?.min !== undefined && num < validation.min) {
|
|
564
|
+
throw new Error(`Amount must be at least ${validation.min}`)
|
|
565
|
+
}
|
|
566
|
+
if (validation?.max !== undefined && num > validation.max) {
|
|
567
|
+
throw new Error(`Amount must be at most ${validation.max}`)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return num
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Delegates to `sanitizeNumber`. @throws same as sanitizeNumber */
|
|
574
|
+
function sanitizeRange(data: any, validation?: any): number {
|
|
575
|
+
return sanitizeNumber(data, validation)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Parses via `new Date()`, returns ISO date string (`YYYY-MM-DD`). Applies
|
|
580
|
+
* min/max date constraints.
|
|
581
|
+
* @throws Error on invalid date or out-of-range
|
|
582
|
+
*/
|
|
583
|
+
function sanitizeDate(data: any, validation?: any): string {
|
|
584
|
+
let dateStr = String(data).trim()
|
|
585
|
+
|
|
586
|
+
// Try to parse as ISO date
|
|
587
|
+
const date = new Date(dateStr)
|
|
588
|
+
if (isNaN(date.getTime())) {
|
|
589
|
+
throw new Error('Invalid date format')
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Return as ISO date string
|
|
593
|
+
const isoDate = date.toISOString().split('T')[0]
|
|
594
|
+
|
|
595
|
+
// Apply min/max constraints
|
|
596
|
+
if (validation?.min) {
|
|
597
|
+
const minDate = new Date(validation.min)
|
|
598
|
+
if (date < minDate) {
|
|
599
|
+
throw new Error(`Date must be on or after ${validation.min}`)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (validation?.max) {
|
|
603
|
+
const maxDate = new Date(validation.max)
|
|
604
|
+
if (date > maxDate) {
|
|
605
|
+
throw new Error(`Date must be on or before ${validation.max}`)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return isoDate
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Parses via `new Date()`, returns full ISO datetime string. Applies
|
|
614
|
+
* min/max datetime constraints.
|
|
615
|
+
* @throws Error on invalid datetime or out-of-range
|
|
616
|
+
*/
|
|
617
|
+
function sanitizeDatetime(data: any, validation?: any): string {
|
|
618
|
+
let dateStr = String(data).trim()
|
|
619
|
+
|
|
620
|
+
// Try to parse as ISO datetime
|
|
621
|
+
const date = new Date(dateStr)
|
|
622
|
+
if (isNaN(date.getTime())) {
|
|
623
|
+
throw new Error('Invalid datetime format')
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Return as ISO datetime string
|
|
627
|
+
const isoDatetime = date.toISOString()
|
|
628
|
+
|
|
629
|
+
// Apply min/max constraints
|
|
630
|
+
if (validation?.min) {
|
|
631
|
+
const minDate = new Date(validation.min)
|
|
632
|
+
if (date < minDate) {
|
|
633
|
+
throw new Error(`Datetime must be on or after ${validation.min}`)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (validation?.max) {
|
|
637
|
+
const maxDate = new Date(validation.max)
|
|
638
|
+
if (date > maxDate) {
|
|
639
|
+
throw new Error(`Datetime must be on or before ${validation.max}`)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return isoDatetime
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Accepts `true/false` booleans or truthy/falsy strings
|
|
648
|
+
* (`'true','1','yes','on'` / `'false','0','no','off'`).
|
|
649
|
+
* @throws Error on unrecognised value
|
|
650
|
+
*/
|
|
651
|
+
function sanitizeBoolean(data: any, validation?: any): boolean {
|
|
652
|
+
if (typeof data === 'boolean') {
|
|
653
|
+
return data
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const str = String(data).toLowerCase()
|
|
657
|
+
if (['true', '1', 'yes', 'on'].includes(str)) {
|
|
658
|
+
return true
|
|
659
|
+
} else if (['false', '0', 'no', 'off'].includes(str)) {
|
|
660
|
+
return false
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
throw new Error('Invalid boolean value')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Delegates to `sanitizeBoolean`. */
|
|
667
|
+
function sanitizeCheckbox(data: any, validation?: any): boolean {
|
|
668
|
+
return sanitizeBoolean(data, validation)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Validates the value against `validation.options` (string[]).
|
|
673
|
+
* @throws Error('Invalid option selected') if value not in allowed list
|
|
674
|
+
*/
|
|
675
|
+
function sanitizeSelect(data: any, validation?: any): string {
|
|
676
|
+
let value = String(data).trim()
|
|
677
|
+
|
|
678
|
+
// Validate against allowed options
|
|
679
|
+
if (validation?.options) {
|
|
680
|
+
// Options are now just an array of strings
|
|
681
|
+
const allowedValues = Array.isArray(validation.options) ? validation.options : []
|
|
682
|
+
if (!allowedValues.includes(value)) {
|
|
683
|
+
throw new Error('Invalid option selected')
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return value
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Accepts array or comma-separated string. Deduplicates. Validates each
|
|
692
|
+
* value against `validation.options`. Truncates to `validation.max`.
|
|
693
|
+
* @throws Error on invalid option or non-array/string input
|
|
694
|
+
*/
|
|
695
|
+
function sanitizeMultiselect(data: any, validation?: any): string[] {
|
|
696
|
+
let values: string[]
|
|
697
|
+
|
|
698
|
+
if (Array.isArray(data)) {
|
|
699
|
+
values = data.map(item => String(item).trim())
|
|
700
|
+
} else if (typeof data === 'string') {
|
|
701
|
+
values = data.split(',').map(item => item.trim())
|
|
702
|
+
} else {
|
|
703
|
+
throw new Error('Multiselect must be an array or comma-separated string')
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Remove duplicates
|
|
707
|
+
values = [...new Set(values)]
|
|
708
|
+
|
|
709
|
+
// Validate against allowed options
|
|
710
|
+
if (validation?.options) {
|
|
711
|
+
// Options are now just an array of strings
|
|
712
|
+
const allowedValues = Array.isArray(validation.options) ? validation.options : []
|
|
713
|
+
for (const value of values) {
|
|
714
|
+
if (!allowedValues.includes(value)) {
|
|
715
|
+
throw new Error(`Invalid option: ${value}`)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Apply max selection count
|
|
721
|
+
if (validation?.max && values.length > validation.max) {
|
|
722
|
+
values = values.slice(0, validation.max)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return values
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Delegates to `sanitizeSelect`. */
|
|
729
|
+
function sanitizeRadio(data: any, validation?: any): string {
|
|
730
|
+
return sanitizeSelect(data, validation)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Validates `#RGB` or `#RRGGBB` hex format. Normalizes 3-digit to 6-digit.
|
|
735
|
+
* Returns uppercase. @throws Error on invalid hex format
|
|
736
|
+
*/
|
|
737
|
+
function sanitizeColor(data: any, validation?: any): string {
|
|
738
|
+
let color = String(data).trim()
|
|
739
|
+
|
|
740
|
+
// Validate hex color format
|
|
741
|
+
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
|
|
742
|
+
if (!hexRegex.test(color)) {
|
|
743
|
+
throw new Error('Invalid color format. Use #RRGGBB or #RGB format')
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Normalize to 6-digit hex
|
|
747
|
+
if (color.length === 4) {
|
|
748
|
+
color = '#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3]
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return color.toUpperCase()
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Validates `data.name` presence, optional maxSize and allowedTypes.
|
|
756
|
+
* Sanitizes filename to `[a-zA-Z0-9.-_]` characters only.
|
|
757
|
+
* @throws Error on invalid file data, size, or type
|
|
758
|
+
*/
|
|
759
|
+
function sanitizeFile(data: any, validation?: any): any {
|
|
760
|
+
// Basic file validation - would need more sophisticated handling in practice
|
|
761
|
+
if (typeof data !== 'object' || !data.name) {
|
|
762
|
+
throw new Error('Invalid file data')
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Validate file size
|
|
766
|
+
if (validation?.maxSize && data.size > validation.maxSize) {
|
|
767
|
+
throw new Error(`File size exceeds maximum of ${validation.maxSize} bytes`)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Validate file type
|
|
771
|
+
if (validation?.allowedTypes && !validation.allowedTypes.includes(data.type)) {
|
|
772
|
+
throw new Error(`File type ${data.type} is not allowed`)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Sanitize filename
|
|
776
|
+
data.name = data.name.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
777
|
+
|
|
778
|
+
return data
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Delegates to `sanitizeFile`. Image dimension validation is a stub
|
|
783
|
+
* (noted for future implementation).
|
|
784
|
+
* @throws same as sanitizeFile
|
|
785
|
+
*/
|
|
786
|
+
function sanitizeImage(data: any, validation?: any): any {
|
|
787
|
+
const file = sanitizeFile(data, validation)
|
|
788
|
+
|
|
789
|
+
// Additional image-specific validation
|
|
790
|
+
if (validation?.maxWidth || validation?.maxHeight) {
|
|
791
|
+
// Would need to actually load and check image dimensions
|
|
792
|
+
// For now, just pass through
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return file
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Parses JSON strings. Rejects payloads containing `'function'`, `'eval'`,
|
|
800
|
+
* or `'script'` strings as a basic code-injection guard.
|
|
801
|
+
* @throws Error on invalid JSON or dangerous content
|
|
802
|
+
*/
|
|
803
|
+
function sanitizeJson(data: any, validation?: any): any {
|
|
804
|
+
if (typeof data === 'string') {
|
|
805
|
+
try {
|
|
806
|
+
data = JSON.parse(data)
|
|
807
|
+
} catch {
|
|
808
|
+
throw new Error('Invalid JSON format')
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Basic security check - prevent code injection
|
|
813
|
+
const jsonStr = JSON.stringify(data)
|
|
814
|
+
if (jsonStr.includes('function') || jsonStr.includes('eval') || jsonStr.includes('script')) {
|
|
815
|
+
throw new Error('JSON contains potentially dangerous content')
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return data
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Validates UUID v1–v5 format. DB-level FK constraints enforce existence.
|
|
823
|
+
* @throws Error('Invalid reference format') on non-UUID input
|
|
824
|
+
*/
|
|
825
|
+
function sanitizeReference(data: any, validation?: any): string {
|
|
826
|
+
let ref = String(data).trim()
|
|
827
|
+
|
|
828
|
+
// Validate UUID format
|
|
829
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
830
|
+
if (!uuidRegex.test(ref)) {
|
|
831
|
+
throw new Error('Invalid reference format')
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Would need to check existence in referenced table
|
|
835
|
+
// For now, just validate format
|
|
836
|
+
|
|
837
|
+
return ref
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Sanitizes each string field of an address object via `sanitizeText`.
|
|
842
|
+
* Passes non-string fields through unchanged.
|
|
843
|
+
* @throws Error if input is not an object
|
|
844
|
+
*/
|
|
845
|
+
function sanitizeAddress(data: any, validation?: any): any {
|
|
846
|
+
if (typeof data !== 'object' || data === null) {
|
|
847
|
+
throw new Error('Address must be an object')
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Sanitize each address component
|
|
851
|
+
const sanitized: any = {}
|
|
852
|
+
for (const [key, value] of Object.entries(data)) {
|
|
853
|
+
if (typeof value === 'string') {
|
|
854
|
+
sanitized[key] = sanitizeText(value)
|
|
855
|
+
} else {
|
|
856
|
+
sanitized[key] = value
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return sanitized
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ─── FORMAT HELPERS ────────────────────────────────────────────────────────────
|
|
864
|
+
|
|
865
|
+
/** Formats an object as a 2-space indented JSON string. */
|
|
866
|
+
function formatJson(data: any): string {
|
|
867
|
+
return JSON.stringify(data, null, 2)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Formats an ISO date string as locale date ('January 15, 2024'). Returns raw data on invalid input. */
|
|
871
|
+
function formatDate(data: string): string {
|
|
872
|
+
const date = new Date(data)
|
|
873
|
+
if (isNaN(date.getTime())) {
|
|
874
|
+
return data
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return date.toLocaleDateString('en-US', {
|
|
878
|
+
year: 'numeric',
|
|
879
|
+
month: 'long',
|
|
880
|
+
day: 'numeric'
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Formats an ISO datetime string as locale date+time ('January 15, 2024, 02:30 PM'). Returns raw on invalid. */
|
|
885
|
+
function formatDatetime(data: string): string {
|
|
886
|
+
const date = new Date(data)
|
|
887
|
+
if (isNaN(date.getTime())) {
|
|
888
|
+
return data
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return date.toLocaleString('en-US', {
|
|
892
|
+
year: 'numeric',
|
|
893
|
+
month: 'long',
|
|
894
|
+
day: 'numeric',
|
|
895
|
+
hour: '2-digit',
|
|
896
|
+
minute: '2-digit'
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** Formats a number as currency using Intl.NumberFormat. Defaults to USD. context.currency_code overrides. */
|
|
901
|
+
function formatCurrency(data: number, context?: any): string {
|
|
902
|
+
const currency = context?.currency_code || 'USD'
|
|
903
|
+
return new Intl.NumberFormat('en-US', {
|
|
904
|
+
style: 'currency',
|
|
905
|
+
currency: currency
|
|
906
|
+
}).format(data)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** Formats 10-digit US numbers as '(NXX) NXX-XXXX'. 11-digit with leading 1 as '+1 (NXX) NXX-XXXX'. Returns raw otherwise. */
|
|
910
|
+
function formatPhone(data: string): string {
|
|
911
|
+
// Basic US phone formatting
|
|
912
|
+
const phone = data.replace(/\D/g, '')
|
|
913
|
+
if (phone.length === 10) {
|
|
914
|
+
return `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6)}`
|
|
915
|
+
} else if (phone.length === 11 && phone[0] === '1') {
|
|
916
|
+
return `+${phone[0]} (${phone.slice(1, 4)}) ${phone.slice(4, 7)}-${phone.slice(7)}`
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return data
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** Pass-through — URLs are already display-ready. */
|
|
923
|
+
function formatUrl(data: string): string {
|
|
924
|
+
return data
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Pass-through — UUID returned as-is; display resolution requires a DB lookup (not done here). */
|
|
928
|
+
function formatReference(data: string, context?: any): string {
|
|
929
|
+
// Would need to look up the referenced entity
|
|
930
|
+
// For now, return the UUID
|
|
931
|
+
return data
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Joins address components (street, city, state, postal_code, country) into a comma-separated string. */
|
|
935
|
+
function formatAddress(data: any): string {
|
|
936
|
+
if (typeof data !== 'object' || data === null) {
|
|
937
|
+
return String(data)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const parts = [
|
|
941
|
+
data.street,
|
|
942
|
+
data.city,
|
|
943
|
+
data.state,
|
|
944
|
+
data.postal_code,
|
|
945
|
+
data.country
|
|
946
|
+
].filter(Boolean)
|
|
947
|
+
|
|
948
|
+
return parts.join(', ')
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/** Joins a string array with ', '. Returns String(data) for non-arrays. */
|
|
952
|
+
function formatMultiselect(data: string[]): string {
|
|
953
|
+
if (!Array.isArray(data)) {
|
|
954
|
+
return String(data)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return data.join(', ')
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Returns 'Active'/'Inactive' when context.field is 'is_active'; otherwise 'Yes'/'No'. */
|
|
961
|
+
function formatBoolean(data: boolean, context?: any): string {
|
|
962
|
+
if (context?.field === 'is_active') {
|
|
963
|
+
return data ? 'Active' : 'Inactive'
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return data ? 'Yes' : 'No'
|
|
967
|
+
}
|