@svadmin/ui 0.70.0 → 0.73.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/README.md +40 -25
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/actions.js +1 -1
- package/dist/app.css +7804 -5188
- package/dist/app.theme.css +7738 -5173
- package/dist/class-properties.generated.d.ts +1 -0
- package/dist/class-properties.generated.js +2 -0
- package/dist/classnames.d.ts +8 -0
- package/dist/classnames.js +26 -0
- package/dist/component-registry.svelte.d.ts +33 -19
- package/dist/component-registry.svelte.js +6 -4
- package/dist/components/AdminApp.svelte +55 -53
- package/dist/components/AdminApp.svelte.d.ts +2 -2
- package/dist/components/ApiSettings.svelte +19 -1
- package/dist/components/AuditLogDrawer.svelte +14 -3
- package/dist/components/AutoForm.svelte +162 -50
- package/dist/components/AutoForm.svelte.d.ts +4 -0
- package/dist/components/AutoTable.svelte +599 -259
- package/dist/components/AutoTable.svelte.d.ts +6 -0
- package/dist/components/BoundRecordDetailDrawer.svelte +172 -0
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +11 -0
- package/dist/components/Breadcrumbs.svelte +7 -4
- package/dist/components/CancelTaskButton.svelte +9 -5
- package/dist/components/ColumnHeaderFilter.svelte +4 -3
- package/dist/components/ColumnSettings.svelte +1 -0
- package/dist/components/ComboboxField.svelte +118 -36
- package/dist/components/ComboboxField.svelte.d.ts +5 -4
- package/dist/components/CreatePage.svelte +7 -1
- package/dist/components/CreatePage.svelte.d.ts +2 -0
- package/dist/components/DateRangeInput.svelte +75 -0
- package/dist/components/DateRangeInput.svelte.d.ts +25 -0
- package/dist/components/DateTimeInput.svelte +65 -0
- package/dist/components/DateTimeInput.svelte.d.ts +19 -0
- package/dist/components/DetailDrawer.svelte +4 -0
- package/dist/components/DetailDrawer.svelte.d.ts +1 -0
- package/dist/components/DevTools.svelte +8 -21
- package/dist/components/DraggableGrid.svelte +1 -0
- package/dist/components/DraggableHeader.svelte +5 -9
- package/dist/components/DraggableHeader.svelte.d.ts +43 -18
- package/dist/components/DraggableRowTable.svelte +2 -2
- package/dist/components/DynamicFormList.svelte +17 -17
- package/dist/components/DynamicFormList.svelte.d.ts +2 -1
- package/dist/components/EditPage.svelte +7 -1
- package/dist/components/EditPage.svelte.d.ts +2 -0
- package/dist/components/EditableTable.svelte +4 -0
- package/dist/components/ErrorPage.svelte +3 -1
- package/dist/components/FieldDisplay.svelte +14 -0
- package/dist/components/FieldDisplay.svelte.d.ts +7 -0
- package/dist/components/FieldRenderer.svelte +96 -20
- package/dist/components/FileUpload.svelte +250 -0
- package/dist/components/FileUpload.svelte.d.ts +32 -0
- package/dist/components/FilterBuilder.svelte +236 -268
- package/dist/components/FilterBuilder.svelte.d.ts +3 -0
- package/dist/components/FilterDropdown.svelte.d.ts +1 -1
- package/dist/components/Header.svelte +5 -6
- package/dist/components/ImportWizard.svelte +182 -94
- package/dist/components/InfiniteList.svelte +22 -8
- package/dist/components/InfiniteList.svelte.d.ts +5 -3
- package/dist/components/InlineEdit.svelte +114 -44
- package/dist/components/JsonSchemaForm.svelte +256 -95
- package/dist/components/JsonSchemaForm.svelte.d.ts +12 -1
- package/dist/components/Layout.svelte +20 -17
- package/dist/components/LazyTaskQueueDrawer.svelte +26 -0
- package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +8 -0
- package/dist/components/ListPage.svelte +11 -6
- package/dist/components/ListPage.svelte.d.ts +2 -0
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/MasterDetailView.svelte +3 -3
- package/dist/components/MediaLibraryModal.svelte +5 -2
- package/dist/components/MentionsInput.svelte +4 -3
- package/dist/components/MoneyInput.svelte +25 -0
- package/dist/components/MoneyInput.svelte.d.ts +20 -0
- package/dist/components/NavigateToResource.svelte +3 -2
- package/dist/components/NumberInput.svelte +25 -0
- package/dist/components/NumberInput.svelte.d.ts +19 -0
- package/dist/components/NumericInput.svelte +110 -0
- package/dist/components/NumericInput.svelte.d.ts +23 -0
- package/dist/components/PercentInput.svelte +25 -0
- package/dist/components/PercentInput.svelte.d.ts +20 -0
- package/dist/components/PermissionMatrix.svelte +4 -3
- package/dist/components/ProfilePage.svelte +2 -2
- package/dist/components/QuickEditDrawer.svelte +42 -0
- package/dist/components/QuickEditDrawer.svelte.d.ts +10 -0
- package/dist/components/RecordDetailDrawer.svelte +16 -108
- package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
- package/dist/components/RecordRowActions.svelte +41 -0
- package/dist/components/RecordRowActions.svelte.d.ts +14 -0
- package/dist/components/ResourceAccessGuard.svelte +7 -5
- package/dist/components/ResourceOperationsPage.svelte +25 -19
- package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
- package/dist/components/RetryTaskButton.svelte +9 -5
- package/dist/components/RowActions.svelte +5 -3
- package/dist/components/SensitiveDataMask.svelte +3 -3
- package/dist/components/SettingsPage.svelte +1 -1
- package/dist/components/ShowInferencer.svelte +4 -2
- package/dist/components/ShowInferencer.svelte.d.ts +2 -0
- package/dist/components/ShowPage.svelte +49 -39
- package/dist/components/ShowPage.svelte.d.ts +2 -0
- package/dist/components/Sidebar.svelte +15 -13
- package/dist/components/SplitPaneLayout.svelte +4 -2
- package/dist/components/SpreadsheetView.svelte +81 -286
- package/dist/components/StepsForm.svelte +166 -49
- package/dist/components/StepsForm.svelte.d.ts +1 -0
- package/dist/components/SvarAutoTable.svelte +39 -0
- package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
- package/dist/components/SvarDataGrid.svelte +362 -0
- package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
- package/dist/components/SvarGridCell.svelte +10 -0
- package/dist/components/SvarGridCell.svelte.d.ts +7 -0
- package/dist/components/SvarResourceRead.svelte +50 -0
- package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
- package/dist/components/SvarResourceTable.svelte +63 -0
- package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
- package/dist/components/SvarResourceTableView.svelte +364 -0
- package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
- package/dist/components/TaskDetails.svelte +20 -7
- package/dist/components/TaskList.svelte +19 -7
- package/dist/components/TaskQueueDrawer.svelte +92 -37
- package/dist/components/Toast.svelte +4 -2
- package/dist/components/TooltipButton.svelte +1 -1
- package/dist/components/TreeSelect.svelte +212 -30
- package/dist/components/TreeSelect.svelte.d.ts +3 -1
- package/dist/components/ValidatedField.svelte +27 -0
- package/dist/components/ValidatedField.svelte.d.ts +35 -0
- package/dist/components/account/ImportMembersPage.svelte +2 -2
- package/dist/components/account/SecurityLogPage.svelte +3 -1
- package/dist/components/account/SettingsEnterprisePage.svelte +7 -5
- package/dist/components/account/TeamMembersPage.svelte +1 -1
- package/dist/components/auto-table-grid.d.ts +26 -0
- package/dist/components/auto-table-grid.js +1 -0
- package/dist/components/buttons/CloneButton.svelte +6 -4
- package/dist/components/buttons/DeleteButton.svelte +95 -29
- package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -0
- package/dist/components/buttons/EditButton.svelte +6 -4
- package/dist/components/buttons/ExportButton.svelte +22 -4
- package/dist/components/buttons/ImportButton.svelte +83 -38
- package/dist/components/buttons/ImportButton.svelte.d.ts +6 -10
- package/dist/components/buttons/RefreshButton.svelte +48 -8
- package/dist/components/buttons/ShowButton.svelte +6 -4
- package/dist/components/content/ApiKeyList.svelte +3 -1
- package/dist/components/content/ContentPageHeader.svelte +12 -9
- package/dist/components/content/ContentPageShell.svelte +8 -9
- package/dist/components/content/FileBrowser.svelte +35 -0
- package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
- package/dist/components/content/FileList.svelte +3 -1
- package/dist/components/content/MemberList.svelte +3 -1
- package/dist/components/content/MetricBlock.svelte +10 -13
- package/dist/components/content/NetworkTable.svelte +6 -4
- package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
- package/dist/components/content/PageToolbar.svelte +6 -10
- package/dist/components/content/SectionHeader.svelte +8 -13
- package/dist/components/content/SecurityEventTable.svelte +3 -1
- package/dist/components/content/SettingsFieldRow.svelte +9 -23
- package/dist/components/content/SettingsGroup.svelte +11 -27
- package/dist/components/content/StatusBadge.svelte +3 -8
- package/dist/components/content/StatusTabs.svelte +2 -2
- package/dist/components/content/WorkspaceLayout.svelte +8 -12
- package/dist/components/enterprise/filter-tree.d.ts +48 -0
- package/dist/components/enterprise/filter-tree.js +224 -0
- package/dist/components/enterprise/json-schema-form.d.ts +39 -0
- package/dist/components/enterprise/json-schema-form.js +195 -0
- package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
- package/dist/components/enterprise/recursive-schema-form.js +196 -0
- package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
- package/dist/components/enterprise/spreadsheet-formula.js +245 -0
- package/dist/components/fieldComponentMap.d.ts +18 -13
- package/dist/components/fieldComponentMap.js +100 -47
- package/dist/components/fields/AvatarField.svelte +1 -1
- package/dist/components/fields/ChoiceField.svelte +16 -0
- package/dist/components/fields/ChoiceField.svelte.d.ts +10 -0
- package/dist/components/fields/CodeField.svelte +11 -12
- package/dist/components/fields/CodeField.svelte.d.ts +1 -1
- package/dist/components/fields/DateField.svelte +4 -3
- package/dist/components/fields/DateRangeField.svelte +2 -4
- package/dist/components/fields/ImageField.svelte +3 -1
- package/dist/components/fields/ImagesField.svelte +23 -0
- package/dist/components/fields/ImagesField.svelte.d.ts +6 -0
- package/dist/components/fields/JsonField.svelte +19 -13
- package/dist/components/fields/PasswordField.svelte +5 -0
- package/dist/components/fields/PasswordField.svelte.d.ts +6 -0
- package/dist/components/fields/date-field-value.d.ts +2 -0
- package/dist/components/fields/date-field-value.js +13 -0
- package/dist/components/fields/json-value.d.ts +11 -0
- package/dist/components/fields/json-value.js +28 -0
- package/dist/components/json-schema-form-state.d.ts +37 -0
- package/dist/components/json-schema-form-state.js +168 -0
- package/dist/components/profile/ActivityTimeline.svelte +2 -2
- package/dist/components/record-detail.svelte.d.ts +19 -0
- package/dist/components/record-detail.svelte.js +34 -0
- package/dist/components/saved-list-views.js +29 -29
- package/dist/components/svar-grid-cells.d.ts +14 -0
- package/dist/components/svar-grid-cells.js +2 -0
- package/dist/components/svar-grid-contract.d.ts +80 -0
- package/dist/components/svar-grid-contract.js +159 -0
- package/dist/components/svar-grid-interactions.d.ts +54 -0
- package/dist/components/svar-grid-interactions.js +174 -0
- package/dist/components/svar-grid-loading.d.ts +40 -0
- package/dist/components/svar-grid-loading.js +180 -0
- package/dist/components/svar-grid-operations.d.ts +37 -0
- package/dist/components/svar-grid-operations.js +186 -0
- package/dist/components/svar-grid-panes.d.ts +34 -0
- package/dist/components/svar-grid-panes.js +66 -0
- package/dist/components/svar-grid-resource-loading.d.ts +18 -0
- package/dist/components/svar-grid-resource-loading.js +68 -0
- package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
- package/dist/components/svar-grid-resource-session.svelte.js +61 -0
- package/dist/components/table-contract.d.ts +13 -0
- package/dist/components/table-contract.js +42 -0
- package/dist/components/task-utils.js +4 -2
- package/dist/components/ui/alert/alert-variants.d.ts +2 -3
- package/dist/components/ui/alert/alert-variants.js +1 -2
- package/dist/components/ui/alert/alert.svelte +4 -5
- package/dist/components/ui/alert/alert.svelte.d.ts +3 -8
- package/dist/components/ui/alert/index.js +0 -2
- package/dist/components/ui/avatar/avatar-variants.d.ts +2 -3
- package/dist/components/ui/avatar/avatar-variants.js +1 -2
- package/dist/components/ui/avatar/avatar.svelte +5 -6
- package/dist/components/ui/avatar/avatar.svelte.d.ts +3 -8
- package/dist/components/ui/badge/badge-variants.d.ts +2 -3
- package/dist/components/ui/badge/badge-variants.js +3 -2
- package/dist/components/ui/badge/badge.svelte +4 -5
- package/dist/components/ui/badge/badge.svelte.d.ts +3 -8
- package/dist/components/ui/badge/index.js +0 -2
- package/dist/components/ui/button/button-variants.d.ts +2 -3
- package/dist/components/ui/button/button-variants.js +5 -3
- package/dist/components/ui/button/button.svelte +100 -44
- package/dist/components/ui/button/button.svelte.d.ts +8 -12
- package/dist/components/ui/button/index.js +0 -2
- package/dist/components/ui/command/command-dialog.svelte +3 -1
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-group.svelte +4 -2
- package/dist/components/ui/command/command-item.svelte +4 -2
- package/dist/components/ui/command/command-separator.svelte +3 -1
- package/dist/components/ui/command/index.d.ts +5 -5
- package/dist/components/ui/input/input.svelte +41 -24
- package/dist/components/ui/input/input.svelte.d.ts +5 -4
- package/dist/components/ui/sheet/sheet.svelte +9 -3
- package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
- package/dist/components/ui/textarea/textarea.svelte +2 -1
- package/dist/components/ui/tooltip/tooltip.svelte +4 -2
- package/dist/components.css +3079 -0
- package/dist/design-contract.d.ts +13 -0
- package/dist/design-contract.js +11 -0
- package/dist/design-system.d.ts +15 -0
- package/dist/design-system.js +7 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.js +9 -0
- package/dist/inline-value.d.ts +5 -0
- package/dist/inline-value.js +24 -0
- package/dist/numeric-input.d.ts +2 -0
- package/dist/numeric-input.js +9 -0
- package/dist/page-chrome.css +12 -49
- package/dist/recipes.d.ts +4 -0
- package/dist/recipes.js +4 -0
- package/dist/rendering/index.d.ts +87 -0
- package/dist/rendering/index.js +117 -0
- package/dist/styled-system/css/conditions.js +36 -0
- package/dist/styled-system/css/css.d.ts +22 -0
- package/dist/styled-system/css/css.js +45 -0
- package/dist/styled-system/css/cva.d.ts +6 -0
- package/dist/styled-system/css/cva.js +87 -0
- package/dist/styled-system/css/cx.d.ts +5 -0
- package/dist/styled-system/css/cx.js +15 -0
- package/dist/styled-system/css/index.d.ts +5 -0
- package/dist/styled-system/css/index.js +4 -0
- package/dist/styled-system/css/sva.d.ts +4 -0
- package/dist/styled-system/css/sva.js +46 -0
- package/dist/styled-system/helpers.js +316 -0
- package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
- package/dist/styled-system/patterns/aspect-ratio.js +38 -0
- package/dist/styled-system/patterns/bleed.d.ts +21 -0
- package/dist/styled-system/patterns/bleed.js +24 -0
- package/dist/styled-system/patterns/box.d.ts +20 -0
- package/dist/styled-system/patterns/box.js +15 -0
- package/dist/styled-system/patterns/center.d.ts +20 -0
- package/dist/styled-system/patterns/center.js +21 -0
- package/dist/styled-system/patterns/circle.d.ts +20 -0
- package/dist/styled-system/patterns/circle.js +25 -0
- package/dist/styled-system/patterns/container.d.ts +20 -0
- package/dist/styled-system/patterns/container.js +21 -0
- package/dist/styled-system/patterns/cq.d.ts +21 -0
- package/dist/styled-system/patterns/cq.js +21 -0
- package/dist/styled-system/patterns/divider.d.ts +22 -0
- package/dist/styled-system/patterns/divider.js +25 -0
- package/dist/styled-system/patterns/flex.d.ts +26 -0
- package/dist/styled-system/patterns/flex.js +26 -0
- package/dist/styled-system/patterns/float.d.ts +23 -0
- package/dist/styled-system/patterns/float.js +52 -0
- package/dist/styled-system/patterns/grid-item.d.ts +25 -0
- package/dist/styled-system/patterns/grid-item.js +25 -0
- package/dist/styled-system/patterns/grid.d.ts +24 -0
- package/dist/styled-system/patterns/grid.js +27 -0
- package/dist/styled-system/patterns/hstack.d.ts +21 -0
- package/dist/styled-system/patterns/hstack.js +24 -0
- package/dist/styled-system/patterns/index.d.ts +21 -0
- package/dist/styled-system/patterns/index.js +20 -0
- package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
- package/dist/styled-system/patterns/link-overlay.js +24 -0
- package/dist/styled-system/patterns/spacer.d.ts +20 -0
- package/dist/styled-system/patterns/spacer.js +25 -0
- package/dist/styled-system/patterns/square.d.ts +20 -0
- package/dist/styled-system/patterns/square.js +24 -0
- package/dist/styled-system/patterns/stack.d.ts +23 -0
- package/dist/styled-system/patterns/stack.js +24 -0
- package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
- package/dist/styled-system/patterns/visually-hidden.js +18 -0
- package/dist/styled-system/patterns/vstack.d.ts +21 -0
- package/dist/styled-system/patterns/vstack.js +24 -0
- package/dist/styled-system/patterns/wrap.d.ts +24 -0
- package/dist/styled-system/patterns/wrap.js +25 -0
- package/dist/styled-system/recipes/content-header.d.ts +31 -0
- package/dist/styled-system/recipes/content-header.js +65 -0
- package/dist/styled-system/recipes/content-page.d.ts +34 -0
- package/dist/styled-system/recipes/content-page.js +43 -0
- package/dist/styled-system/recipes/create-recipe.js +82 -0
- package/dist/styled-system/recipes/index.d.ts +17 -0
- package/dist/styled-system/recipes/index.js +16 -0
- package/dist/styled-system/recipes/metric-block.d.ts +34 -0
- package/dist/styled-system/recipes/metric-block.js +76 -0
- package/dist/styled-system/recipes/product-list.d.ts +31 -0
- package/dist/styled-system/recipes/product-list.js +73 -0
- package/dist/styled-system/recipes/product-section.d.ts +31 -0
- package/dist/styled-system/recipes/product-section.js +49 -0
- package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
- package/dist/styled-system/recipes/product-settings-row.js +58 -0
- package/dist/styled-system/recipes/product-settings.d.ts +31 -0
- package/dist/styled-system/recipes/product-settings.js +57 -0
- package/dist/styled-system/recipes/product-status.d.ts +34 -0
- package/dist/styled-system/recipes/product-status.js +45 -0
- package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
- package/dist/styled-system/recipes/product-toolbar.js +41 -0
- package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
- package/dist/styled-system/recipes/product-workspace.js +58 -0
- package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
- package/dist/styled-system/recipes/surface-metric.js +63 -0
- package/dist/styled-system/recipes/surface-table.d.ts +34 -0
- package/dist/styled-system/recipes/surface-table.js +62 -0
- package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
- package/dist/styled-system/recipes/ui-badge.js +38 -0
- package/dist/styled-system/recipes/ui-button.d.ts +32 -0
- package/dist/styled-system/recipes/ui-button.js +43 -0
- package/dist/styled-system/recipes/ui-input.d.ts +31 -0
- package/dist/styled-system/recipes/ui-input.js +49 -0
- package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
- package/dist/styled-system/recipes/ui-textarea.js +24 -0
- package/dist/styled-system/styles.css +1257 -0
- package/dist/styled-system/tokens/index.d.ts +9 -0
- package/dist/styled-system/tokens/index.js +268 -0
- package/dist/styled-system/tokens/tokens.d.ts +36 -0
- package/dist/styled-system/types/composition.d.ts +227 -0
- package/dist/styled-system/types/conditions.d.ts +236 -0
- package/dist/styled-system/types/csstype.d.ts +22570 -0
- package/dist/styled-system/types/index.d.ts +6 -0
- package/dist/styled-system/types/parts.d.ts +8 -0
- package/dist/styled-system/types/pattern.d.ts +78 -0
- package/dist/styled-system/types/prop-type.d.ts +223 -0
- package/dist/styled-system/types/recipe.d.ts +181 -0
- package/dist/styled-system/types/selectors.d.ts +59 -0
- package/dist/styled-system/types/static-css.d.ts +56 -0
- package/dist/styled-system/types/style-props.d.ts +8088 -0
- package/dist/styled-system/types/system-types.d.ts +151 -0
- package/dist/styles/aliases.css +49 -0
- package/dist/styles/compatibility.epilogue.css +384 -0
- package/dist/styles/compatibility.prelude.css +5513 -0
- package/dist/styles/conditional-states.css +28 -0
- package/dist/styles/conditional-utilities.css +22 -0
- package/dist/styles/recipes.css +1262 -0
- package/dist/types.d.ts +3 -3
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +38 -14
- package/styles-compatibility.json +12 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const rootKeys = new Set(['$schema', '$id', '$comment', 'title', 'description', 'type', 'properties', 'required', 'additionalProperties']);
|
|
2
|
+
const fieldKeys = new Set(['$id', '$comment', 'title', 'description', 'type', 'enum', 'const', 'default', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength', 'readOnly', 'writeOnly', 'deprecated', 'examples']);
|
|
3
|
+
const unsafeKeys = new Set(['__proto__', 'constructor', 'prototype']);
|
|
4
|
+
const pointer = (key) => `/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
5
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
6
|
+
const scalar = (value) => value === null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value));
|
|
7
|
+
const same = (a, b) => a === b;
|
|
8
|
+
function matches(field, value) {
|
|
9
|
+
if (value === null)
|
|
10
|
+
return field.nullable;
|
|
11
|
+
if (field.type === 'enum')
|
|
12
|
+
return scalar(value);
|
|
13
|
+
if (field.type === 'integer')
|
|
14
|
+
return typeof value === 'number' && Number.isSafeInteger(value);
|
|
15
|
+
if (field.type === 'number')
|
|
16
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
17
|
+
return typeof value === field.type;
|
|
18
|
+
}
|
|
19
|
+
export function readSchemaForm(schema) {
|
|
20
|
+
const model = { fields: [], issues: [], additionalProperties: true };
|
|
21
|
+
const issue = (path, code) => { model.issues.push({ path, code }); };
|
|
22
|
+
if (!record(schema)) {
|
|
23
|
+
issue('', 'invalid-schema');
|
|
24
|
+
return model;
|
|
25
|
+
}
|
|
26
|
+
for (const key of Object.keys(schema))
|
|
27
|
+
if (!rootKeys.has(key))
|
|
28
|
+
issue(pointer(key), 'unsupported-schema');
|
|
29
|
+
if (schema['type'] !== undefined && schema['type'] !== 'object')
|
|
30
|
+
issue('/type', 'unsupported-schema');
|
|
31
|
+
if (schema['additionalProperties'] !== undefined && typeof schema['additionalProperties'] !== 'boolean')
|
|
32
|
+
issue('/additionalProperties', 'unsupported-schema');
|
|
33
|
+
model.additionalProperties = schema['additionalProperties'] !== false;
|
|
34
|
+
const properties = schema['properties'] === undefined ? {} : schema['properties'];
|
|
35
|
+
if (!record(properties) || Object.keys(properties).length > 256) {
|
|
36
|
+
issue('/properties', 'invalid-schema');
|
|
37
|
+
return model;
|
|
38
|
+
}
|
|
39
|
+
const required = schema['required'] === undefined ? [] : schema['required'];
|
|
40
|
+
if (!Array.isArray(required) || required.length > 256 || required.some((key) => typeof key !== 'string' || !Object.hasOwn(properties, key)))
|
|
41
|
+
issue('/required', 'invalid-schema');
|
|
42
|
+
const requiredKeys = new Set(Array.isArray(required) ? required : []);
|
|
43
|
+
for (const [key, raw] of Object.entries(properties)) {
|
|
44
|
+
const path = pointer(key);
|
|
45
|
+
if (unsafeKeys.has(key) || !record(raw)) {
|
|
46
|
+
issue(path, 'invalid-schema');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const keyword of Object.keys(raw))
|
|
50
|
+
if (!fieldKeys.has(keyword))
|
|
51
|
+
issue(`${path}${pointer(keyword)}`, 'unsupported-schema');
|
|
52
|
+
let type = raw['type'];
|
|
53
|
+
let nullable = false;
|
|
54
|
+
if (Array.isArray(type)) {
|
|
55
|
+
nullable = type.includes('null');
|
|
56
|
+
const nonNull = type.filter((item) => item !== 'null');
|
|
57
|
+
if (type.length !== 2 || nonNull.length !== 1)
|
|
58
|
+
issue(path, 'unsupported-schema');
|
|
59
|
+
type = nonNull[0];
|
|
60
|
+
}
|
|
61
|
+
const hasEnum = Object.hasOwn(raw, 'enum');
|
|
62
|
+
const hasConst = Object.hasOwn(raw, 'const');
|
|
63
|
+
const rawChoices = hasEnum ? raw['enum'] : hasConst ? [raw['const']] : undefined;
|
|
64
|
+
let choices;
|
|
65
|
+
if (rawChoices !== undefined) {
|
|
66
|
+
if (!Array.isArray(rawChoices) || rawChoices.length === 0 || rawChoices.length > 1000 || !rawChoices.every(scalar))
|
|
67
|
+
issue(path, 'invalid-schema');
|
|
68
|
+
else {
|
|
69
|
+
choices = [...rawChoices];
|
|
70
|
+
if (hasConst)
|
|
71
|
+
choices = choices.filter((item) => same(item, raw['const']));
|
|
72
|
+
if (!choices.length)
|
|
73
|
+
issue(path, 'invalid-schema');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (hasEnum)
|
|
77
|
+
issue(path, 'invalid-schema');
|
|
78
|
+
if (type === undefined)
|
|
79
|
+
type = choices ? 'enum' : 'string';
|
|
80
|
+
else if (type === 'enum') {
|
|
81
|
+
issue(path, 'invalid-schema');
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!['string', 'number', 'integer', 'boolean', 'enum'].includes(String(type))) {
|
|
85
|
+
issue(path, 'unsupported-schema');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (type === 'enum')
|
|
89
|
+
nullable = choices?.includes(null) ?? false;
|
|
90
|
+
const field = {
|
|
91
|
+
key, title: String(raw['title'] ?? key), description: String(raw['description'] ?? ''),
|
|
92
|
+
type: type, nullable, required: requiredKeys.has(key), readonly: raw['readOnly'] === true,
|
|
93
|
+
choices, hasDefault: Object.hasOwn(raw, 'default'), defaultValue: scalar(raw['default']) ? raw['default'] : undefined,
|
|
94
|
+
minimum: undefined, maximum: undefined, exclusiveMinimum: undefined, exclusiveMaximum: undefined,
|
|
95
|
+
multipleOf: undefined, minLength: undefined, maxLength: undefined,
|
|
96
|
+
};
|
|
97
|
+
for (const constraint of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength']) {
|
|
98
|
+
const value = raw[constraint];
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
if (typeof value !== 'number' || !Number.isFinite(value)
|
|
102
|
+
|| (constraint === 'multipleOf' && value <= 0)
|
|
103
|
+
|| ((constraint === 'minLength' || constraint === 'maxLength') && (!Number.isSafeInteger(value) || value < 0)))
|
|
104
|
+
issue(`${path}/${constraint}`, 'invalid-schema');
|
|
105
|
+
else
|
|
106
|
+
field[constraint] = value;
|
|
107
|
+
}
|
|
108
|
+
if (choices?.some((choice) => !matches(field, choice)))
|
|
109
|
+
issue(path, 'invalid-schema');
|
|
110
|
+
if (field.hasDefault && (!scalar(raw['default']) || !matches(field, raw['default']) || (choices && !choices.some((choice) => same(choice, raw['default'])))))
|
|
111
|
+
issue(path, 'invalid-schema');
|
|
112
|
+
model.fields.push(field);
|
|
113
|
+
}
|
|
114
|
+
return model;
|
|
115
|
+
}
|
|
116
|
+
/** 只为不存在的键补默认值;显式 undefined 表示用户已清空,不重新灌回默认值。 */
|
|
117
|
+
export function schemaFormValues(model, value) {
|
|
118
|
+
const result = { ...value };
|
|
119
|
+
for (const field of model.fields)
|
|
120
|
+
if (!Object.hasOwn(value, field.key) && field.hasDefault)
|
|
121
|
+
result[field.key] = field.defaultValue;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
export function parseSchemaNumber(raw) {
|
|
125
|
+
if (raw.trim() === '')
|
|
126
|
+
return undefined;
|
|
127
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(raw.trim()))
|
|
128
|
+
throw new Error('invalid-value');
|
|
129
|
+
const value = Number(raw);
|
|
130
|
+
if (!Number.isFinite(value))
|
|
131
|
+
throw new Error('invalid-value');
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
export function enumIndex(choices, value) {
|
|
135
|
+
const index = choices.findIndex((choice) => same(choice, value));
|
|
136
|
+
return index < 0 ? '' : String(index);
|
|
137
|
+
}
|
|
138
|
+
export function enumValue(choices, index) {
|
|
139
|
+
if (index === '')
|
|
140
|
+
return undefined;
|
|
141
|
+
if (!/^(0|[1-9]\d*)$/.test(index))
|
|
142
|
+
throw new Error('invalid-value');
|
|
143
|
+
const value = choices[Number(index)];
|
|
144
|
+
if (value === undefined)
|
|
145
|
+
throw new Error('invalid-value');
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
export function validateSchemaForm(model, value) {
|
|
149
|
+
const issues = [...model.issues];
|
|
150
|
+
if (!record(value))
|
|
151
|
+
return [...issues, { path: '', code: 'invalid-value' }];
|
|
152
|
+
for (const field of model.fields) {
|
|
153
|
+
const path = pointer(field.key), current = Object.hasOwn(value, field.key) ? value[field.key] : undefined;
|
|
154
|
+
const issue = (code) => { issues.push({ path, code }); };
|
|
155
|
+
if (current === undefined) {
|
|
156
|
+
if (field.required)
|
|
157
|
+
issue('required');
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!matches(field, current)) {
|
|
161
|
+
issue('type');
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (field.choices && !field.choices.some((choice) => same(choice, current)))
|
|
165
|
+
issue('enum');
|
|
166
|
+
if (typeof current === 'number') {
|
|
167
|
+
if ((field.minimum !== undefined && current < field.minimum) || (field.exclusiveMinimum !== undefined && current <= field.exclusiveMinimum))
|
|
168
|
+
issue('minimum');
|
|
169
|
+
if ((field.maximum !== undefined && current > field.maximum) || (field.exclusiveMaximum !== undefined && current >= field.exclusiveMaximum))
|
|
170
|
+
issue('maximum');
|
|
171
|
+
if (field.multipleOf !== undefined) {
|
|
172
|
+
const quotient = current / field.multipleOf;
|
|
173
|
+
if (!Number.isFinite(quotient) || Math.abs(quotient - Math.round(quotient)) > Math.min(1e-8, Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4))
|
|
174
|
+
issue('multipleOf');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (typeof current === 'string') {
|
|
178
|
+
const length = Array.from(current).length;
|
|
179
|
+
if (field.minLength !== undefined && length < field.minLength)
|
|
180
|
+
issue('minLength');
|
|
181
|
+
if (field.maxLength !== undefined && length > field.maxLength)
|
|
182
|
+
issue('maxLength');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const keys = new Set(model.fields.map((field) => field.key));
|
|
186
|
+
for (const [key, current] of Object.entries(value)) {
|
|
187
|
+
if (unsafeKeys.has(key))
|
|
188
|
+
issues.push({ path: pointer(key), code: 'invalid-value' });
|
|
189
|
+
if (!model.additionalProperties && !keys.has(key) && current !== undefined)
|
|
190
|
+
issues.push({ path: pointer(key), code: 'additionalProperties' });
|
|
191
|
+
}
|
|
192
|
+
return issues;
|
|
193
|
+
}
|
|
194
|
+
// Reuse the hardened recursive form transport boundary, including accessor/symbol rejection.
|
|
195
|
+
export { schemaFormSnapshot } from '../json-schema-form-state.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type SchemaFormModel, type SchemaFormIssue } from './json-schema-form.js';
|
|
2
|
+
type Node = {
|
|
3
|
+
kind: 'object';
|
|
4
|
+
properties: Record<string, Node>;
|
|
5
|
+
required: Set<string>;
|
|
6
|
+
additional: boolean;
|
|
7
|
+
nullable: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'array';
|
|
10
|
+
item: Node;
|
|
11
|
+
min: number | undefined;
|
|
12
|
+
max: number | undefined;
|
|
13
|
+
nullable: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'scalar';
|
|
16
|
+
model: SchemaFormModel;
|
|
17
|
+
};
|
|
18
|
+
export interface RecursiveSchemaFormModel {
|
|
19
|
+
node: Node | undefined;
|
|
20
|
+
issues: SchemaFormIssue[];
|
|
21
|
+
}
|
|
22
|
+
export declare const schemaFormPointer: (path: readonly string[]) => string;
|
|
23
|
+
/** Compile bounded objects/arrays, delegating scalar assertions to the reviewed leaf validator.
|
|
24
|
+
* Unsupported assertions fail closed; this is deliberately not a complete JSON Schema engine.
|
|
25
|
+
* TypeBox's symbol annotations are not JSON assertion keywords and are not executed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readRecursiveSchemaForm(schema: unknown): RecursiveSchemaFormModel;
|
|
28
|
+
/** Validate a detached JSON payload without defaults, coercion, cleaning or partial acceptance. */
|
|
29
|
+
export declare function validateRecursiveSchemaForm(model: RecursiveSchemaFormModel, value: unknown): SchemaFormIssue[];
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readSchemaForm, validateSchemaForm } from './json-schema-form.js';
|
|
2
|
+
import { SCHEMA_FORM_LIMITS, schemaFormSnapshot } from '../json-schema-form-state.js';
|
|
3
|
+
const annotationKeys = ['$schema', '$id', '$comment', 'title', 'description', 'type', 'default', 'readOnly', 'writeOnly', 'deprecated', 'examples'];
|
|
4
|
+
const objectKeys = new Set([...annotationKeys, 'properties', 'required', 'additionalProperties']);
|
|
5
|
+
const arrayKeys = new Set([...annotationKeys, 'items', 'minItems', 'maxItems']);
|
|
6
|
+
const unsafe = new Set(['__proto__', 'prototype', 'constructor']);
|
|
7
|
+
export const schemaFormPointer = (path) => path.map(key => '/' + key.replaceAll('~', '~0').replaceAll('/', '~1')).join('');
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
&& (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
11
|
+
}
|
|
12
|
+
function dataRecord(value) {
|
|
13
|
+
return record(value) && Object.keys(value).every(key => {
|
|
14
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
15
|
+
return descriptor !== undefined && 'value' in descriptor;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Compile bounded objects/arrays, delegating scalar assertions to the reviewed leaf validator.
|
|
19
|
+
* Unsupported assertions fail closed; this is deliberately not a complete JSON Schema engine.
|
|
20
|
+
* TypeBox's symbol annotations are not JSON assertion keywords and are not executed.
|
|
21
|
+
*/
|
|
22
|
+
export function readRecursiveSchemaForm(schema) {
|
|
23
|
+
const issues = [];
|
|
24
|
+
const active = new Set();
|
|
25
|
+
let visited = 0;
|
|
26
|
+
const issue = (path, code) => {
|
|
27
|
+
if (issues.length < 128)
|
|
28
|
+
issues.push({ path, code });
|
|
29
|
+
};
|
|
30
|
+
function read(raw, path, depth, root = false) {
|
|
31
|
+
if (++visited > SCHEMA_FORM_LIMITS.nodes || depth > SCHEMA_FORM_LIMITS.depth || !dataRecord(raw) || active.has(raw)) {
|
|
32
|
+
issue(path, 'invalid-schema');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
active.add(raw);
|
|
36
|
+
try {
|
|
37
|
+
let type = raw['type'];
|
|
38
|
+
let nullable = false;
|
|
39
|
+
if (Array.isArray(type)) {
|
|
40
|
+
const concrete = type.filter(value => value !== 'null');
|
|
41
|
+
if (type.length !== 2 || concrete.length !== 1 || !type.includes('null')) {
|
|
42
|
+
issue(path, 'unsupported-schema');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
nullable = true;
|
|
46
|
+
type = concrete[0];
|
|
47
|
+
}
|
|
48
|
+
if (type === undefined && (root || Object.hasOwn(raw, 'properties')))
|
|
49
|
+
type = 'object';
|
|
50
|
+
if (root && type !== 'object') {
|
|
51
|
+
issue(path, 'unsupported-schema');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const key of ['readOnly', 'writeOnly', 'deprecated']) {
|
|
55
|
+
if (raw[key] !== undefined && typeof raw[key] !== 'boolean')
|
|
56
|
+
issue(path + '/' + key, 'invalid-schema');
|
|
57
|
+
}
|
|
58
|
+
if (Object.hasOwn(raw, 'default')) {
|
|
59
|
+
try {
|
|
60
|
+
schemaFormSnapshot({ value: raw['default'] });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
issue(path + '/default', 'invalid-schema');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (type !== 'object' && type !== 'array') {
|
|
67
|
+
const model = readSchemaForm({ type: 'object', properties: { value: raw } });
|
|
68
|
+
for (const failure of model.issues)
|
|
69
|
+
issue(path + failure.path.replace(/^\/value(?=\/|$)/u, ''), failure.code);
|
|
70
|
+
return { kind: 'scalar', model };
|
|
71
|
+
}
|
|
72
|
+
const allowed = type === 'object' ? objectKeys : arrayKeys;
|
|
73
|
+
for (const key of Object.keys(raw))
|
|
74
|
+
if (!allowed.has(key))
|
|
75
|
+
issue(path + '/' + key, 'unsupported-schema');
|
|
76
|
+
if (type === 'object') {
|
|
77
|
+
const properties = raw['properties'] ?? {};
|
|
78
|
+
if (!dataRecord(properties) || Object.keys(properties).length > 256 || raw['properties'] === null) {
|
|
79
|
+
issue(path + '/properties', 'invalid-schema');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const keys = raw['required'] ?? [];
|
|
83
|
+
if (!Array.isArray(keys) || keys.length > 256 || raw['required'] === null
|
|
84
|
+
|| new Set(keys).size !== keys.length || keys.some(key => typeof key !== 'string' || !Object.hasOwn(properties, key))) {
|
|
85
|
+
issue(path + '/required', 'invalid-schema');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (raw['additionalProperties'] !== undefined && typeof raw['additionalProperties'] !== 'boolean') {
|
|
89
|
+
issue(path + '/additionalProperties', 'unsupported-schema');
|
|
90
|
+
}
|
|
91
|
+
const result = Object.create(null);
|
|
92
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
93
|
+
const childPath = path + schemaFormPointer([key]);
|
|
94
|
+
if (unsafe.has(key)) {
|
|
95
|
+
issue(childPath, 'invalid-schema');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const node = read(child, childPath, depth + 1);
|
|
99
|
+
if (node)
|
|
100
|
+
result[key] = node;
|
|
101
|
+
}
|
|
102
|
+
return { kind: 'object', properties: result, required: new Set(keys), additional: raw['additionalProperties'] !== false, nullable };
|
|
103
|
+
}
|
|
104
|
+
const item = read(raw['items'], path + '/items', depth + 1);
|
|
105
|
+
const definition = raw;
|
|
106
|
+
function bound(key) {
|
|
107
|
+
const value = definition[key];
|
|
108
|
+
if (value === undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
111
|
+
issue(path + '/' + key, 'invalid-schema');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
const min = bound('minItems'), max = bound('maxItems');
|
|
117
|
+
if (min !== undefined && max !== undefined && min > max)
|
|
118
|
+
issue(path, 'invalid-schema');
|
|
119
|
+
return item ? { kind: 'array', item, min, max, nullable } : undefined;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
active.delete(raw);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let node;
|
|
126
|
+
try {
|
|
127
|
+
node = read(schema, '', 0, true);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
issue('', 'invalid-schema');
|
|
131
|
+
}
|
|
132
|
+
return { node, issues };
|
|
133
|
+
}
|
|
134
|
+
/** Validate a detached JSON payload without defaults, coercion, cleaning or partial acceptance. */
|
|
135
|
+
export function validateRecursiveSchemaForm(model, value) {
|
|
136
|
+
if (model.issues.length || !model.node)
|
|
137
|
+
return model.issues.length ? [...model.issues] : [{ path: '', code: 'invalid-schema' }];
|
|
138
|
+
const issues = [];
|
|
139
|
+
const issue = (path, code) => {
|
|
140
|
+
if (issues.length < 128)
|
|
141
|
+
issues.push({ path, code });
|
|
142
|
+
};
|
|
143
|
+
let payload;
|
|
144
|
+
try {
|
|
145
|
+
payload = schemaFormSnapshot(value);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [{ path: '', code: 'invalid-value' }];
|
|
149
|
+
}
|
|
150
|
+
let visited = 0;
|
|
151
|
+
function visit(node, current, path, required, depth) {
|
|
152
|
+
if (++visited > SCHEMA_FORM_LIMITS.nodes || depth > SCHEMA_FORM_LIMITS.depth) {
|
|
153
|
+
issue(path, 'invalid-value');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (current === undefined) {
|
|
157
|
+
if (required)
|
|
158
|
+
issue(path, 'required');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (node.kind === 'scalar') {
|
|
162
|
+
for (const failure of validateSchemaForm(node.model, { value: current }))
|
|
163
|
+
issue(path + failure.path.replace(/^\/value(?=\/|$)/u, ''), failure.code);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (current === null && node.nullable)
|
|
167
|
+
return;
|
|
168
|
+
if (node.kind === 'object') {
|
|
169
|
+
if (!record(current)) {
|
|
170
|
+
issue(path, 'type');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
174
|
+
visit(child, Object.hasOwn(current, key) ? current[key] : undefined, path + schemaFormPointer([key]), node.required.has(key), depth + 1);
|
|
175
|
+
}
|
|
176
|
+
if (!node.additional)
|
|
177
|
+
for (const key of Object.keys(current)) {
|
|
178
|
+
if (!Object.hasOwn(node.properties, key))
|
|
179
|
+
issue(path + schemaFormPointer([key]), 'additionalProperties');
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(current)) {
|
|
184
|
+
issue(path, 'type');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (node.min !== undefined && current.length < node.min)
|
|
188
|
+
issue(path, 'minItems');
|
|
189
|
+
if (node.max !== undefined && current.length > node.max)
|
|
190
|
+
issue(path, 'maxItems');
|
|
191
|
+
for (const [index, item] of current.entries())
|
|
192
|
+
visit(node.item, item, path + '/' + index, true, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
visit(model.node, payload, '', true, 0);
|
|
195
|
+
return issues;
|
|
196
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** 有界、无动态代码执行的轻量公式解析器;不是完整 Excel 公式引擎。 */
|
|
2
|
+
export declare const FORMULA_LIMITS: Readonly<{
|
|
3
|
+
length: 2048;
|
|
4
|
+
tokens: 1024;
|
|
5
|
+
depth: 64;
|
|
6
|
+
work: 10000;
|
|
7
|
+
range: 10000;
|
|
8
|
+
}>;
|
|
9
|
+
export type FormulaErrorCode = '#VALUE!' | '#REF!' | '#NAME?' | '#DIV/0!' | '#NUM!' | '#CYCLE!' | '#LIMIT!';
|
|
10
|
+
export declare function evaluateFormula(raw: string, cells: Readonly<Record<string, string>>): number | string;
|
|
11
|
+
/** 文本导出不能把危险前缀当成外部电子表格公式;调用者仍需完成 CSV 引号转义。 */
|
|
12
|
+
export declare function safeSpreadsheetCsvText(value: string): string;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** 有界、无动态代码执行的轻量公式解析器;不是完整 Excel 公式引擎。 */
|
|
2
|
+
export const FORMULA_LIMITS = Object.freeze({ length: 2048, tokens: 1024, depth: 64, work: 10000, range: 10000 });
|
|
3
|
+
class FormulaError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code) { super(code); this.code = code; }
|
|
6
|
+
}
|
|
7
|
+
function fail(code) { throw new FormulaError(code); }
|
|
8
|
+
function finite(value) { return Number.isFinite(value) ? value : fail('#NUM!'); }
|
|
9
|
+
function numeric(value) {
|
|
10
|
+
if (value === null)
|
|
11
|
+
return 0;
|
|
12
|
+
return typeof value === 'number' ? value : fail('#VALUE!');
|
|
13
|
+
}
|
|
14
|
+
const decimal = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
15
|
+
function address(key) {
|
|
16
|
+
const match = /^\$?([A-Z]+)\$?(\d+)$/i.exec(key);
|
|
17
|
+
if (!match?.[1] || !match[2])
|
|
18
|
+
return fail('#REF!');
|
|
19
|
+
let column = 0;
|
|
20
|
+
for (const char of match[1].toUpperCase()) {
|
|
21
|
+
column = column * 26 + char.charCodeAt(0) - 64;
|
|
22
|
+
if (column > 16384)
|
|
23
|
+
return fail('#REF!');
|
|
24
|
+
}
|
|
25
|
+
const row = Number(match[2]);
|
|
26
|
+
if (row < 1 || row > 1048576 || !Number.isSafeInteger(row))
|
|
27
|
+
return fail('#REF!');
|
|
28
|
+
return { column, row };
|
|
29
|
+
}
|
|
30
|
+
function columnName(column) {
|
|
31
|
+
let name = '';
|
|
32
|
+
while (column > 0) {
|
|
33
|
+
column--;
|
|
34
|
+
name = String.fromCharCode(65 + column % 26) + name;
|
|
35
|
+
column = Math.floor(column / 26);
|
|
36
|
+
}
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
class Evaluation {
|
|
40
|
+
work = 0;
|
|
41
|
+
depth = 0;
|
|
42
|
+
active = new Set();
|
|
43
|
+
cache = new Map();
|
|
44
|
+
cells;
|
|
45
|
+
constructor(cells) { this.cells = cells; }
|
|
46
|
+
tick() { if (++this.work > FORMULA_LIMITS.work)
|
|
47
|
+
fail('#LIMIT!'); }
|
|
48
|
+
bounded(fn) {
|
|
49
|
+
if (++this.depth > FORMULA_LIMITS.depth) {
|
|
50
|
+
this.depth--;
|
|
51
|
+
return fail('#LIMIT!');
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return fn();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
this.depth--;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
cell(key) {
|
|
61
|
+
this.tick();
|
|
62
|
+
const parsed = address(key);
|
|
63
|
+
const normalized = `${columnName(parsed.column)}${parsed.row}`;
|
|
64
|
+
if (this.active.has(normalized))
|
|
65
|
+
return fail('#CYCLE!');
|
|
66
|
+
if (this.cache.has(normalized))
|
|
67
|
+
return this.cache.get(normalized) ?? null;
|
|
68
|
+
const raw = Object.hasOwn(this.cells, normalized) ? this.cells[normalized] : '';
|
|
69
|
+
if (typeof raw !== 'string')
|
|
70
|
+
return fail('#VALUE!');
|
|
71
|
+
this.active.add(normalized);
|
|
72
|
+
try {
|
|
73
|
+
const trimmed = raw.trim();
|
|
74
|
+
const value = raw.startsWith('=')
|
|
75
|
+
? new Parser(raw.slice(1), this).parse()
|
|
76
|
+
: trimmed === '' ? null : decimal.test(trimmed) ? finite(Number(trimmed)) : raw;
|
|
77
|
+
this.cache.set(normalized, value);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
this.active.delete(normalized);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
range(start, end) {
|
|
85
|
+
const a = address(start);
|
|
86
|
+
const b = address(end);
|
|
87
|
+
const left = Math.min(a.column, b.column), right = Math.max(a.column, b.column);
|
|
88
|
+
const top = Math.min(a.row, b.row), bottom = Math.max(a.row, b.row);
|
|
89
|
+
if ((right - left + 1) * (bottom - top + 1) > FORMULA_LIMITS.range)
|
|
90
|
+
return fail('#LIMIT!');
|
|
91
|
+
const values = [];
|
|
92
|
+
for (let row = top; row <= bottom; row++) {
|
|
93
|
+
for (let column = left; column <= right; column++)
|
|
94
|
+
values.push(this.cell(`${columnName(column)}${row}`));
|
|
95
|
+
}
|
|
96
|
+
return values;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
class Parser {
|
|
100
|
+
tokens = [];
|
|
101
|
+
position = 0;
|
|
102
|
+
context;
|
|
103
|
+
constructor(source, context) {
|
|
104
|
+
this.context = context;
|
|
105
|
+
if (source.length + 1 > FORMULA_LIMITS.length)
|
|
106
|
+
fail('#LIMIT!');
|
|
107
|
+
let rest = source;
|
|
108
|
+
while (rest.length) {
|
|
109
|
+
rest = rest.trimStart();
|
|
110
|
+
if (!rest)
|
|
111
|
+
break;
|
|
112
|
+
context.tick();
|
|
113
|
+
if (this.tokens.length >= FORMULA_LIMITS.tokens)
|
|
114
|
+
fail('#LIMIT!');
|
|
115
|
+
const number = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i.exec(rest)?.[0];
|
|
116
|
+
const cell = /^\$?[A-Z]+\$?\d+/i.exec(rest)?.[0];
|
|
117
|
+
const name = /^[A-Z_][A-Z_0-9]*/i.exec(rest)?.[0];
|
|
118
|
+
let token;
|
|
119
|
+
if (number)
|
|
120
|
+
token = { kind: 'number', text: number };
|
|
121
|
+
else if (cell)
|
|
122
|
+
token = { kind: 'cell', text: cell.toUpperCase() };
|
|
123
|
+
else if (name)
|
|
124
|
+
token = { kind: 'name', text: name.toUpperCase() };
|
|
125
|
+
else if (/^[+\-*/(),:]/.test(rest))
|
|
126
|
+
token = { kind: 'symbol', text: rest[0] ?? '' };
|
|
127
|
+
else
|
|
128
|
+
return fail('#VALUE!');
|
|
129
|
+
this.tokens.push(token);
|
|
130
|
+
rest = rest.slice(token.text.length);
|
|
131
|
+
}
|
|
132
|
+
this.tokens.push({ kind: 'end', text: '' });
|
|
133
|
+
}
|
|
134
|
+
peek() { return this.tokens[this.position] ?? { kind: 'end', text: '' }; }
|
|
135
|
+
take() { this.context.tick(); const token = this.peek(); this.position++; return token; }
|
|
136
|
+
expect(text) { if (this.take().text !== text)
|
|
137
|
+
fail('#VALUE!'); }
|
|
138
|
+
parse() {
|
|
139
|
+
return this.context.bounded(() => {
|
|
140
|
+
const result = this.expression();
|
|
141
|
+
if (this.peek().kind !== 'end')
|
|
142
|
+
return fail('#VALUE!');
|
|
143
|
+
return result;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
expression() {
|
|
147
|
+
let value = this.product();
|
|
148
|
+
while (this.peek().text === '+' || this.peek().text === '-') {
|
|
149
|
+
const op = this.take().text;
|
|
150
|
+
const right = numeric(this.product()), left = numeric(value);
|
|
151
|
+
value = finite(op === '+' ? left + right : left - right);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
product() {
|
|
156
|
+
let value = this.unary();
|
|
157
|
+
while (this.peek().text === '*' || this.peek().text === '/') {
|
|
158
|
+
const op = this.take().text;
|
|
159
|
+
const right = numeric(this.unary()), left = numeric(value);
|
|
160
|
+
if (op === '/' && right === 0)
|
|
161
|
+
return fail('#DIV/0!');
|
|
162
|
+
value = finite(op === '*' ? left * right : left / right);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
unary() {
|
|
167
|
+
if (this.peek().text === '+' || this.peek().text === '-') {
|
|
168
|
+
const negative = this.take().text === '-';
|
|
169
|
+
return this.context.bounded(() => finite(numeric(this.unary()) * (negative ? -1 : 1)));
|
|
170
|
+
}
|
|
171
|
+
return this.primary();
|
|
172
|
+
}
|
|
173
|
+
primary() {
|
|
174
|
+
const token = this.take();
|
|
175
|
+
if (token.kind === 'number')
|
|
176
|
+
return finite(Number(token.text));
|
|
177
|
+
if (token.kind === 'cell')
|
|
178
|
+
return this.context.cell(token.text);
|
|
179
|
+
if (token.text === '(')
|
|
180
|
+
return this.context.bounded(() => {
|
|
181
|
+
const value = this.expression();
|
|
182
|
+
this.expect(')');
|
|
183
|
+
return value;
|
|
184
|
+
});
|
|
185
|
+
if (token.kind === 'name')
|
|
186
|
+
return this.context.bounded(() => this.aggregate(token.text));
|
|
187
|
+
return fail('#VALUE!');
|
|
188
|
+
}
|
|
189
|
+
aggregate(name) {
|
|
190
|
+
if (!['SUM', 'AVG', 'AVERAGE', 'COUNT', 'MIN', 'MAX'].includes(name))
|
|
191
|
+
return fail('#NAME?');
|
|
192
|
+
this.expect('(');
|
|
193
|
+
const values = [];
|
|
194
|
+
if (this.peek().text !== ')') {
|
|
195
|
+
while (true) {
|
|
196
|
+
if (this.peek().kind === 'cell' && this.tokens[this.position + 1]?.text === ':') {
|
|
197
|
+
const start = this.take().text;
|
|
198
|
+
this.expect(':');
|
|
199
|
+
const end = this.take();
|
|
200
|
+
if (end.kind !== 'cell')
|
|
201
|
+
return fail('#REF!');
|
|
202
|
+
for (const value of this.context.range(start, end.text))
|
|
203
|
+
if (typeof value === 'number')
|
|
204
|
+
values.push(value);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const value = this.expression();
|
|
208
|
+
if (typeof value === 'number')
|
|
209
|
+
values.push(value);
|
|
210
|
+
}
|
|
211
|
+
if (this.peek().text !== ',')
|
|
212
|
+
break;
|
|
213
|
+
this.take();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.expect(')');
|
|
217
|
+
if (name === 'COUNT')
|
|
218
|
+
return values.length;
|
|
219
|
+
if (name === 'MIN' || name === 'MAX') {
|
|
220
|
+
if (!values.length)
|
|
221
|
+
return 0;
|
|
222
|
+
return values.reduce((a, b) => name === 'MIN' ? Math.min(a, b) : Math.max(a, b));
|
|
223
|
+
}
|
|
224
|
+
const sum = finite(values.reduce((a, b) => finite(a + b), 0));
|
|
225
|
+
if (name === 'SUM')
|
|
226
|
+
return sum;
|
|
227
|
+
return values.length ? finite(sum / values.length) : fail('#DIV/0!');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function evaluateFormula(raw, cells) {
|
|
231
|
+
if (!raw.startsWith('='))
|
|
232
|
+
return raw;
|
|
233
|
+
try {
|
|
234
|
+
return new Parser(raw.slice(1), new Evaluation(cells)).parse() ?? 0;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (error instanceof FormulaError)
|
|
238
|
+
return error.code;
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** 文本导出不能把危险前缀当成外部电子表格公式;调用者仍需完成 CSV 引号转义。 */
|
|
243
|
+
export function safeSpreadsheetCsvText(value) {
|
|
244
|
+
return /^[\s\uFEFF]*[=+\-@]/u.test(value) || /^[\t\r\n]/.test(value) ? `'${value}` : value;
|
|
245
|
+
}
|