@michaelthielemann/kestrel 1.2.1
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/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +234 -0
- package/kestrel.config.ts +28 -0
- package/layers/access/nuxt.config.ts +5 -0
- package/layers/access/server/middleware/00.ip-allowlist.ts +67 -0
- package/layers/access/server/middleware/access-guard.ts +30 -0
- package/layers/access/server/types.d.ts +8 -0
- package/layers/access/server/utils/csrf.ts +39 -0
- package/layers/access/server/utils/grant-registry.ts +36 -0
- package/layers/access/server/utils/guard.ts +44 -0
- package/layers/access/server/utils/ip-allowlist.ts +76 -0
- package/layers/access/server/utils/policy.ts +98 -0
- package/layers/access/server/utils/public-resources.ts +11 -0
- package/layers/access/server/utils/render-context.ts +41 -0
- package/layers/access/server/utils/require-admin.ts +15 -0
- package/layers/admin/app/components/AdminAccount.vue +212 -0
- package/layers/admin/app/components/AdminNav.vue +44 -0
- package/layers/admin/app/components/BlockFields.vue +68 -0
- package/layers/admin/app/components/BlockPreview.vue +401 -0
- package/layers/admin/app/components/BlockTree.vue +433 -0
- package/layers/admin/app/components/BlocksBody.vue +162 -0
- package/layers/admin/app/components/CollectionDeleteDialog.vue +38 -0
- package/layers/admin/app/components/CollectionEditor.vue +203 -0
- package/layers/admin/app/components/CollectionList.vue +1091 -0
- package/layers/admin/app/components/EditorStatus.vue +199 -0
- package/layers/admin/app/components/EditorUnsupported.vue +28 -0
- package/layers/admin/app/components/FieldsBody.vue +23 -0
- package/layers/admin/app/components/LocaleBar.vue +105 -0
- package/layers/admin/app/components/PageFields.vue +101 -0
- package/layers/admin/app/components/PageFieldsPane.vue +39 -0
- package/layers/admin/app/components/SeoFields.vue +169 -0
- package/layers/admin/app/components/SingletonEditor.vue +92 -0
- package/layers/admin/app/composables/useAuth.ts +68 -0
- package/layers/admin/app/composables/useBlockTree.ts +95 -0
- package/layers/admin/app/composables/useBlocks.ts +7 -0
- package/layers/admin/app/composables/useCachedResource.ts +35 -0
- package/layers/admin/app/composables/useCollectionOps.ts +53 -0
- package/layers/admin/app/composables/useCollections.ts +7 -0
- package/layers/admin/app/composables/useContentLocales.ts +23 -0
- package/layers/admin/app/composables/useEditForm.ts +370 -0
- package/layers/admin/app/composables/useLinkResolver.ts +40 -0
- package/layers/admin/app/composables/useListColumns.ts +53 -0
- package/layers/admin/app/composables/useListUrlState.ts +53 -0
- package/layers/admin/app/composables/usePendingPublishPoll.ts +15 -0
- package/layers/admin/app/composables/usePreviewViewport.ts +30 -0
- package/layers/admin/app/composables/usePublishStatus.ts +98 -0
- package/layers/admin/app/composables/useRailCollapsed.ts +14 -0
- package/layers/admin/app/composables/useTheme.ts +36 -0
- package/layers/admin/app/composables/useUnsavedGuard.ts +22 -0
- package/layers/admin/app/layouts/admin.vue +267 -0
- package/layers/admin/app/middleware/admin-auth.ts +4 -0
- package/layers/admin/app/pages/admin/[collection]/[id].vue +202 -0
- package/layers/admin/app/pages/admin/[collection]/index.vue +62 -0
- package/layers/admin/app/pages/admin/index.vue +118 -0
- package/layers/admin/app/pages/admin/login.vue +68 -0
- package/layers/admin/app/pages/admin/media.vue +28 -0
- package/layers/admin/app/pages/admin/references.vue +146 -0
- package/layers/admin/app/plugins/admin-style-guard.client.ts +76 -0
- package/layers/admin/app/plugins/reauth.client.ts +20 -0
- package/layers/admin/app/utils/admin-style-guard.ts +20 -0
- package/layers/admin/app/utils/block-tree.ts +214 -0
- package/layers/admin/app/utils/collection-ops.ts +44 -0
- package/layers/admin/app/utils/dead-refs.ts +30 -0
- package/layers/admin/app/utils/edit-form.ts +223 -0
- package/layers/admin/app/utils/editor-expose.ts +26 -0
- package/layers/admin/app/utils/editor-form-context.ts +83 -0
- package/layers/admin/app/utils/editor-registry.ts +20 -0
- package/layers/admin/app/utils/field-keys.ts +11 -0
- package/layers/admin/app/utils/list-columns.ts +141 -0
- package/layers/admin/app/utils/list-query.ts +96 -0
- package/layers/admin/app/utils/nav-active.ts +9 -0
- package/layers/admin/app/utils/populate-blocks.ts +288 -0
- package/layers/admin/app/utils/preview-channel.ts +62 -0
- package/layers/admin/app/utils/preview-viewport.ts +80 -0
- package/layers/admin/app/utils/reauth.ts +68 -0
- package/layers/admin/app/utils/record-title.ts +15 -0
- package/layers/admin/app/utils/register-builtin-editors.ts +9 -0
- package/layers/admin/app/utils/safe-redirect.ts +12 -0
- package/layers/admin/nuxt.config.ts +17 -0
- package/layers/auth/nuxt.config.ts +1 -0
- package/layers/auth/server/api/auth/login.post.ts +38 -0
- package/layers/auth/server/api/auth/logout.post.ts +9 -0
- package/layers/auth/server/api/auth/session.get.ts +1 -0
- package/layers/auth/server/utils/admin-credential.ts +42 -0
- package/layers/auth/server/utils/client-ip.ts +88 -0
- package/layers/auth/server/utils/login-throttle.ts +126 -0
- package/layers/auth/server/utils/password.ts +52 -0
- package/layers/auth/server/utils/session-cookie.ts +44 -0
- package/layers/auth/server/utils/session-epoch.ts +96 -0
- package/layers/auth/server/utils/session.ts +85 -0
- package/layers/collections/nuxt.config.ts +1 -0
- package/layers/collections/server/collections/pages.ts +19 -0
- package/layers/collections/server/plugins/01.register.ts +32 -0
- package/layers/collections/server/plugins/02.register-relation-populate.ts +17 -0
- package/layers/collections/server/utils/populate-relations.ts +66 -0
- package/layers/core/app/utils/bulk-actions.ts +9 -0
- package/layers/core/app/utils/filter-ops.ts +98 -0
- package/layers/core/app/utils/list-limits.ts +27 -0
- package/layers/core/app/utils/locale-path.ts +12 -0
- package/layers/core/app/utils/slugify.ts +16 -0
- package/layers/core/modules/auto-discovery/extract-block.ts +176 -0
- package/layers/core/modules/auto-discovery/index.ts +44 -0
- package/layers/core/modules/auto-discovery/scan.ts +78 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +12 -0
- package/layers/core/modules/kestrel/index.ts +86 -0
- package/layers/core/nuxt.config.ts +8 -0
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +8 -0
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +4 -0
- package/layers/core/server/api/[collection]/[id].delete.ts +5 -0
- package/layers/core/server/api/[collection]/[id].get.ts +8 -0
- package/layers/core/server/api/[collection]/[id].patch.ts +8 -0
- package/layers/core/server/api/[collection]/bulk.post.ts +37 -0
- package/layers/core/server/api/[collection]/index.get.ts +19 -0
- package/layers/core/server/api/[collection]/index.post.ts +8 -0
- package/layers/core/server/api/[collection]/index.put.ts +7 -0
- package/layers/core/server/api/[collection]/options.get.ts +18 -0
- package/layers/core/server/api/[collection]/translations.get.ts +25 -0
- package/layers/core/server/api/collections/[name].get.ts +4 -0
- package/layers/core/server/api/collections/index.get.ts +3 -0
- package/layers/core/server/api/references/broken.get.ts +3 -0
- package/layers/core/server/api/references/referrers.get.ts +24 -0
- package/layers/core/server/database/record-refs.ts +20 -0
- package/layers/core/server/plugins/00.migrate.ts +11 -0
- package/layers/core/server/plugins/02.schema-sync.ts +63 -0
- package/layers/core/server/plugins/03.record-refs.ts +12 -0
- package/layers/core/server/schema/bootstrap.ts +41 -0
- package/layers/core/server/schema/desired.ts +99 -0
- package/layers/core/server/schema/dev-sync.ts +23 -0
- package/layers/core/server/schema/dialect.ts +59 -0
- package/layers/core/server/schema/diff.ts +110 -0
- package/layers/core/server/schema/introspect.ts +67 -0
- package/layers/core/server/schema/model.ts +61 -0
- package/layers/core/server/schema/render-sqlite.ts +89 -0
- package/layers/core/server/schema/sync.ts +201 -0
- package/layers/core/server/tasks/db/migrate.ts +54 -0
- package/layers/core/server/utils/blocks.ts +6 -0
- package/layers/core/server/utils/collection-types.ts +27 -0
- package/layers/core/server/utils/crud.ts +577 -0
- package/layers/core/server/utils/db.ts +26 -0
- package/layers/core/server/utils/defineCollection.ts +207 -0
- package/layers/core/server/utils/duplicate.ts +162 -0
- package/layers/core/server/utils/field-layout.ts +125 -0
- package/layers/core/server/utils/filter-predicate.ts +101 -0
- package/layers/core/server/utils/http.ts +50 -0
- package/layers/core/server/utils/if-unmodified.ts +13 -0
- package/layers/core/server/utils/kestrel-config.ts +410 -0
- package/layers/core/server/utils/key-lock.ts +33 -0
- package/layers/core/server/utils/locale.ts +53 -0
- package/layers/core/server/utils/page-route.ts +58 -0
- package/layers/core/server/utils/page-slug.ts +127 -0
- package/layers/core/server/utils/picker.ts +82 -0
- package/layers/core/server/utils/populate.ts +48 -0
- package/layers/core/server/utils/prerender-memo.ts +23 -0
- package/layers/core/server/utils/read-capture.ts +37 -0
- package/layers/core/server/utils/record-ref-index.ts +200 -0
- package/layers/core/server/utils/registry.ts +24 -0
- package/layers/core/server/utils/resolve-scope.ts +79 -0
- package/layers/core/server/utils/seo.ts +24 -0
- package/layers/core/server/utils/serialize-collection.ts +149 -0
- package/layers/core/server/utils/server-config.ts +18 -0
- package/layers/core/server/utils/sql.ts +2 -0
- package/layers/core/server/utils/storage.local.ts +117 -0
- package/layers/core/server/utils/storage.s3.ts +197 -0
- package/layers/core/server/utils/storage.ts +90 -0
- package/layers/core/server/utils/write-events.ts +31 -0
- package/layers/fields/app/utils/condition.ts +71 -0
- package/layers/fields/app/utils/define-block.ts +13 -0
- package/layers/fields/app/utils/field-constraints.ts +17 -0
- package/layers/fields/app/utils/field-factories.ts +129 -0
- package/layers/fields/app/utils/field-meta.ts +33 -0
- package/layers/fields/app/utils/field-validate.ts +75 -0
- package/layers/fields/app/utils/field-value.ts +7 -0
- package/layers/fields/app/utils/richtext-links.ts +48 -0
- package/layers/fields/nuxt.config.ts +1 -0
- package/layers/fields/server/api/blocks/index.get.ts +9 -0
- package/layers/fields/server/field-registry/index.ts +248 -0
- package/layers/fields/server/field-registry/naming.ts +10 -0
- package/layers/fields/server/field-registry/sanitize.ts +40 -0
- package/layers/fields/server/field-registry/types.ts +19 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +11 -0
- package/layers/fields/server/utils/block-ids.ts +43 -0
- package/layers/fields/server/utils/block-populate.ts +41 -0
- package/layers/fields/server/utils/buildCollection.ts +88 -0
- package/layers/fields/server/utils/buildFieldSchema.ts +22 -0
- package/layers/fields/server/utils/buildTable.ts +107 -0
- package/layers/fields/server/utils/conditional-required.ts +25 -0
- package/layers/fields/server/utils/defineBlock.ts +86 -0
- package/layers/fields/server/utils/defineFieldType.ts +33 -0
- package/layers/fields/server/utils/extract-refs.ts +152 -0
- package/layers/fields/server/utils/field-populate.ts +68 -0
- package/layers/media/app/components/KestrelImg.vue +65 -0
- package/layers/media/app/components/MediaConflictDialog.vue +35 -0
- package/layers/media/app/components/MediaDeleteDialog.vue +52 -0
- package/layers/media/app/components/MediaGrid.vue +113 -0
- package/layers/media/app/components/MediaImage.vue +70 -0
- package/layers/media/app/components/MediaLibrary.vue +451 -0
- package/layers/media/app/components/MediaNewFolderDialog.vue +32 -0
- package/layers/media/app/components/MediaPathBar.vue +163 -0
- package/layers/media/app/components/MediaPicker.vue +19 -0
- package/layers/media/app/components/MediaRenameDialog.vue +32 -0
- package/layers/media/app/components/MediaTable.vue +75 -0
- package/layers/media/app/components/MediaThumb.vue +25 -0
- package/layers/media/app/components/MediaToolbar.vue +90 -0
- package/layers/media/app/components/MediaUploadDialog.vue +87 -0
- package/layers/media/app/components/MediaViewer.vue +88 -0
- package/layers/media/app/components/field/Media.vue +261 -0
- package/layers/media/app/composables/useBlurUp.ts +55 -0
- package/layers/media/app/composables/useMediaClipboard.ts +14 -0
- package/layers/media/app/composables/useMediaContextMenu.ts +56 -0
- package/layers/media/app/composables/useMediaDnd.ts +56 -0
- package/layers/media/app/composables/useMediaLibrary.ts +151 -0
- package/layers/media/app/composables/useMediaOps.ts +50 -0
- package/layers/media/app/composables/useMediaRelocate.ts +39 -0
- package/layers/media/app/composables/useMediaResolver.ts +50 -0
- package/layers/media/app/composables/useMediaUpload.ts +93 -0
- package/layers/media/app/composables/useMediaVariant.ts +34 -0
- package/layers/media/app/plugins/field-media.client.ts +6 -0
- package/layers/media/app/utils/dnd.ts +69 -0
- package/layers/media/app/utils/library.ts +79 -0
- package/layers/media/app/utils/ops.ts +62 -0
- package/layers/media/app/utils/picture-model.ts +125 -0
- package/layers/media/app/utils/thumbhash.ts +19 -0
- package/layers/media/app/utils/variant-name.ts +20 -0
- package/layers/media/nuxt.config.ts +3 -0
- package/layers/media/server/api/media/[id]/usages.get.ts +8 -0
- package/layers/media/server/api/media/[id].delete.ts +18 -0
- package/layers/media/server/api/media/[id].get.ts +10 -0
- package/layers/media/server/api/media/[id].patch.ts +41 -0
- package/layers/media/server/api/media/backfill.post.ts +18 -0
- package/layers/media/server/api/media/copy.post.ts +7 -0
- package/layers/media/server/api/media/delete.post.ts +13 -0
- package/layers/media/server/api/media/folders.post.ts +18 -0
- package/layers/media/server/api/media/index.get.ts +15 -0
- package/layers/media/server/api/media/index.post.ts +157 -0
- package/layers/media/server/api/media/library.get.ts +17 -0
- package/layers/media/server/api/media/move.post.ts +7 -0
- package/layers/media/server/api/media/rename.post.ts +7 -0
- package/layers/media/server/api/media/resolve.get.ts +13 -0
- package/layers/media/server/collections/media-settings.ts +25 -0
- package/layers/media/server/collections/media.ts +29 -0
- package/layers/media/server/database/folders.ts +10 -0
- package/layers/media/server/middleware/ondemand-variants.ts +37 -0
- package/layers/media/server/plugins/02.register-media.ts +19 -0
- package/layers/media/server/plugins/03.media-cleanup.ts +20 -0
- package/layers/media/server/plugins/04.variant-capture.ts +23 -0
- package/layers/media/server/schema-tables/folders.ts +4 -0
- package/layers/media/server/tasks/media/backfill.ts +18 -0
- package/layers/media/server/utils/backfill.ts +132 -0
- package/layers/media/server/utils/derive.ts +59 -0
- package/layers/media/server/utils/folder-paths.ts +42 -0
- package/layers/media/server/utils/folders.ts +12 -0
- package/layers/media/server/utils/library.ts +90 -0
- package/layers/media/server/utils/media-cleanup.ts +27 -0
- package/layers/media/server/utils/media-enabled.ts +23 -0
- package/layers/media/server/utils/media-ops.ts +118 -0
- package/layers/media/server/utils/media-write.ts +13 -0
- package/layers/media/server/utils/naming.ts +52 -0
- package/layers/media/server/utils/ondemand.ts +69 -0
- package/layers/media/server/utils/persist-upload.ts +62 -0
- package/layers/media/server/utils/populate.ts +47 -0
- package/layers/media/server/utils/record.ts +63 -0
- package/layers/media/server/utils/relocate-ops.ts +341 -0
- package/layers/media/server/utils/relocate-plan.ts +63 -0
- package/layers/media/server/utils/resolve.ts +86 -0
- package/layers/media/server/utils/sanitize-svg.ts +12 -0
- package/layers/media/server/utils/sniff.ts +68 -0
- package/layers/media/server/utils/storage-relocate.ts +139 -0
- package/layers/media/server/utils/translations.ts +19 -0
- package/layers/media/server/utils/usages.ts +66 -0
- package/layers/media/server/utils/variant-capture.ts +70 -0
- package/layers/media/server/utils/variants.ts +108 -0
- package/layers/public/app/app.vue +5 -0
- package/layers/public/app/components/BlockRenderer.vue +56 -0
- package/layers/public/app/components/KestrelLink.vue +26 -0
- package/layers/public/app/components/KestrelPreviewBridge.vue +133 -0
- package/layers/public/app/composables/public-page.ts +24 -0
- package/layers/public/app/error.vue +12 -0
- package/layers/public/app/layouts/default.vue +5 -0
- package/layers/public/app/pages/[...slug].vue +157 -0
- package/layers/public/app/pages/__kestrel/preview.vue +34 -0
- package/layers/public/app/utils/block-edit-context.ts +16 -0
- package/layers/public/app/utils/link-href.ts +46 -0
- package/layers/public/app/utils/page-head.ts +84 -0
- package/layers/public/app/utils/preview-protocol.ts +81 -0
- package/layers/public/app/utils/public-route.ts +46 -0
- package/layers/public/modules/deploy-output/deploy-output.ts +370 -0
- package/layers/public/modules/deploy-output/index.ts +82 -0
- package/layers/public/modules/prerender-routes/discover.ts +37 -0
- package/layers/public/modules/prerender-routes/index.ts +85 -0
- package/layers/public/modules/prerender-routes/routes.ts +16 -0
- package/layers/public/modules/prune-media/index.ts +48 -0
- package/layers/public/modules/prune-media/prune-media.ts +81 -0
- package/layers/public/nuxt.config.ts +11 -0
- package/layers/public/server/api/links/resolve.get.ts +19 -0
- package/layers/public/server/api/publish-status.get.ts +44 -0
- package/layers/public/server/api/route.get.ts +18 -0
- package/layers/public/server/database/publish-deps.ts +12 -0
- package/layers/public/server/database/publish-status.ts +19 -0
- package/layers/public/server/plugins/02.register-links.ts +17 -0
- package/layers/public/server/plugins/zz.publish.ts +63 -0
- package/layers/public/server/routes/llms.txt.get.ts +72 -0
- package/layers/public/server/routes/robots.txt.get.ts +8 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +66 -0
- package/layers/public/server/schema-tables/publish-deps.ts +4 -0
- package/layers/public/server/schema-tables/publish-status.ts +4 -0
- package/layers/public/server/tasks/publish/run.ts +26 -0
- package/layers/public/server/utils/link-resolve.ts +35 -0
- package/layers/public/server/utils/llms.ts +39 -0
- package/layers/public/server/utils/page-resolve.ts +86 -0
- package/layers/public/server/utils/populate-links.ts +66 -0
- package/layers/public/server/utils/publish/coalesce.ts +22 -0
- package/layers/public/server/utils/publish/deps-persistence.ts +43 -0
- package/layers/public/server/utils/publish/deps.ts +87 -0
- package/layers/public/server/utils/publish/invalidation.ts +141 -0
- package/layers/public/server/utils/publish/publish-status.ts +54 -0
- package/layers/public/server/utils/publish/publisher.ts +291 -0
- package/layers/public/server/utils/publish/queue.ts +67 -0
- package/layers/public/server/utils/publish/route-for-record.ts +16 -0
- package/layers/public/server/utils/publish/route-keys.ts +13 -0
- package/layers/public/server/utils/site-url.ts +32 -0
- package/layers/public/server/utils/sitemap.ts +97 -0
- package/layers/ui/app/assets/scss/_base.scss +16 -0
- package/layers/ui/app/assets/scss/_datepicker.scss +82 -0
- package/layers/ui/app/assets/scss/_mixins.scss +50 -0
- package/layers/ui/app/assets/scss/_reset.scss +65 -0
- package/layers/ui/app/assets/scss/_tokens.scss +183 -0
- package/layers/ui/app/assets/scss/main.scss +4 -0
- package/layers/ui/app/components/field/Boolean.vue +30 -0
- package/layers/ui/app/components/field/Choice.vue +68 -0
- package/layers/ui/app/components/field/Datetime.vue +96 -0
- package/layers/ui/app/components/field/DeadRefNote.vue +28 -0
- package/layers/ui/app/components/field/Json.vue +63 -0
- package/layers/ui/app/components/field/Layout.vue +140 -0
- package/layers/ui/app/components/field/Link.vue +112 -0
- package/layers/ui/app/components/field/LinkInternalPicker.vue +95 -0
- package/layers/ui/app/components/field/Number.vue +30 -0
- package/layers/ui/app/components/field/Relation.vue +49 -0
- package/layers/ui/app/components/field/Renderer.vue +30 -0
- package/layers/ui/app/components/field/Repeater.vue +460 -0
- package/layers/ui/app/components/field/Richtext.vue +26 -0
- package/layers/ui/app/components/field/Slug.vue +66 -0
- package/layers/ui/app/components/field/Text.vue +35 -0
- package/layers/ui/app/components/field/Unsupported.vue +34 -0
- package/layers/ui/app/components/ui/Alert.vue +50 -0
- package/layers/ui/app/components/ui/Brand.vue +22 -0
- package/layers/ui/app/components/ui/Button.vue +116 -0
- package/layers/ui/app/components/ui/ButtonGroup.vue +96 -0
- package/layers/ui/app/components/ui/Checkbox.vue +52 -0
- package/layers/ui/app/components/ui/CheckboxGroup.vue +50 -0
- package/layers/ui/app/components/ui/Combobox.vue +343 -0
- package/layers/ui/app/components/ui/DatePicker.vue +92 -0
- package/layers/ui/app/components/ui/DateRangePicker.vue +100 -0
- package/layers/ui/app/components/ui/Dialog.vue +107 -0
- package/layers/ui/app/components/ui/EmptyState.vue +46 -0
- package/layers/ui/app/components/ui/Field.vue +57 -0
- package/layers/ui/app/components/ui/Fieldset.vue +64 -0
- package/layers/ui/app/components/ui/Icon.vue +44 -0
- package/layers/ui/app/components/ui/Menu.vue +54 -0
- package/layers/ui/app/components/ui/NumberInput.vue +173 -0
- package/layers/ui/app/components/ui/Richtext.vue +119 -0
- package/layers/ui/app/components/ui/RichtextToolbar.vue +253 -0
- package/layers/ui/app/components/ui/Select.vue +51 -0
- package/layers/ui/app/components/ui/TextInput.vue +110 -0
- package/layers/ui/app/components/ui/Textarea.vue +38 -0
- package/layers/ui/app/components/ui/TimeInput.vue +17 -0
- package/layers/ui/app/components/ui/Toasts.vue +103 -0
- package/layers/ui/app/components/ui/Tooltip.vue +45 -0
- package/layers/ui/app/composables/useAdminLang.ts +8 -0
- package/layers/ui/app/composables/useDragReorder.ts +41 -0
- package/layers/ui/app/composables/useEchoGuard.ts +28 -0
- package/layers/ui/app/composables/useLinkField.ts +70 -0
- package/layers/ui/app/composables/useRecordOptions.ts +84 -0
- package/layers/ui/app/composables/useRepeater.ts +99 -0
- package/layers/ui/app/composables/useT.ts +32 -0
- package/layers/ui/app/composables/useToast.ts +46 -0
- package/layers/ui/app/i18n/de.ts +406 -0
- package/layers/ui/app/i18n/en.ts +440 -0
- package/layers/ui/app/utils/boolean-field.ts +10 -0
- package/layers/ui/app/utils/field-component.ts +22 -0
- package/layers/ui/app/utils/field-empty.ts +21 -0
- package/layers/ui/app/utils/field-registry.ts +36 -0
- package/layers/ui/app/utils/humanize.ts +14 -0
- package/layers/ui/app/utils/icons.ts +147 -0
- package/layers/ui/app/utils/iso-date.ts +18 -0
- package/layers/ui/app/utils/localized.ts +13 -0
- package/layers/ui/app/utils/record-context.ts +15 -0
- package/layers/ui/app/utils/reorder.ts +8 -0
- package/layers/ui/app/utils/richtext-preserve-class.ts +31 -0
- package/layers/ui/app/utils/useFieldA11y.ts +40 -0
- package/layers/ui/nuxt.config.ts +5 -0
- package/nuxt.config.ts +105 -0
- package/package.json +122 -0
- package/scripts/hash-password.mjs +26 -0
- package/scripts/typecheck.mjs +51 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type IntrospectDb } from './introspect'
|
|
2
|
+
import { diffSchema } from './diff'
|
|
3
|
+
import { sqlite, type Dialect } from './dialect'
|
|
4
|
+
import type { SchemaSnapshot, SchemaOp } from './model'
|
|
5
|
+
|
|
6
|
+
export interface SyncDb extends IntrospectDb {
|
|
7
|
+
exec(sql: string): unknown
|
|
8
|
+
transaction(fn: () => void): () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Destructive ops drop or rebuild a table (potential data loss); they require an explicit opt-in (ADR-0002). */
|
|
12
|
+
export function isDestructive(op: SchemaOp): boolean {
|
|
13
|
+
return op.type === 'drop_table' || op.type === 'rebuild_table'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Human description of an op, for dry-run reports and the dev skipped-warning so data loss is visible. */
|
|
17
|
+
export function describeOp(op: SchemaOp): string {
|
|
18
|
+
if (op.type === 'drop_table') return `drop table \`${op.name}\``
|
|
19
|
+
if (op.type === 'rebuild_table') {
|
|
20
|
+
// Name the carried columns + any transform sources (not just a count), so the operator can see exactly
|
|
21
|
+
// what survives — everything else that is live but NOT in this list has its data dropped by the rebuild.
|
|
22
|
+
const carried = op.copy.join(', ') || '—'
|
|
23
|
+
const sources = [...new Set(Object.values(op.transforms ?? {}).map((t) => t.from))]
|
|
24
|
+
const from = sources.length ? ` (+ transform source(s): ${sources.join(', ')})` : ''
|
|
25
|
+
return `rebuild table \`${op.table.name}\` — keeps: ${carried}${from}; any other live column's data is dropped`
|
|
26
|
+
}
|
|
27
|
+
if (op.type === 'create_table') return `create table \`${op.table.name}\` (${op.table.columns.length} column(s))`
|
|
28
|
+
if (op.type === 'add_column') return `add column \`${op.table}\`.\`${op.column.name}\` ${op.column.type}`
|
|
29
|
+
if (op.type === 'create_index') return `create index \`${op.index.name}\` on \`${op.index.table}\` (${op.index.columns.join(', ')})`
|
|
30
|
+
if (op.type === 'drop_index') return `drop index \`${op.index.name}\` on \`${op.index.table}\``
|
|
31
|
+
if (op.type === 'rename_column') return `rename column \`${op.table}\`.\`${op.from}\` to \`${op.to}\``
|
|
32
|
+
return (op as SchemaOp).type
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reasons SQLite would reject a rebuild's `INSERT…SELECT` — surfaced up-front as a clear error instead
|
|
37
|
+
* of an opaque "NOT NULL constraint failed" inside an aborted transaction:
|
|
38
|
+
* - a NOT NULL column with no default that isn't carried over (a new required field on a populated table)
|
|
39
|
+
* - a carried-over column that became NOT NULL but whose existing rows still hold NULL
|
|
40
|
+
* Identifier quoting comes from the dialect (the feasibility probes run against the live DB).
|
|
41
|
+
*/
|
|
42
|
+
function rebuildProblems(db: SyncDb, op: Extract<SchemaOp, { type: 'rebuild_table' }>, quote: Dialect['quote'], renames: Map<string, string>): string[] {
|
|
43
|
+
const carried = new Set(op.copy)
|
|
44
|
+
const problems: string[] = []
|
|
45
|
+
// The probes run against the LIVE table, which still holds the PRE-migration column names: rename_column
|
|
46
|
+
// ops and the rebuild's INSERT…SELECT only run later, inside the transaction. So resolve each carried
|
|
47
|
+
// column to its live SOURCE expression — a transform's `from`, else its own name — and skip when no live
|
|
48
|
+
// source exists yet (a plain rename we can't map here), rather than probe a name that doesn't exist and
|
|
49
|
+
// abort the whole migration with `no such column`. The expressions mirror render-sqlite's `copySource`.
|
|
50
|
+
const liveCols = new Set((db.pragma(`table_info("${op.table.name.replace(/"/g, '""')}")`) as { name: string }[]).map((r) => r.name))
|
|
51
|
+
const liveSourceIsNull = (c: string): boolean | null => {
|
|
52
|
+
const t = op.transforms?.[c]
|
|
53
|
+
if (t) {
|
|
54
|
+
// `wrap` (single→multiple) coalesces NULL to '[]' → never NULL. `unwrap` (multiple→single) is
|
|
55
|
+
// `json_extract(from, '$[0]')` → NULL for a NULL or empty source array.
|
|
56
|
+
if (t.type === 'wrap') return false
|
|
57
|
+
if (!liveCols.has(t.from)) return null
|
|
58
|
+
return db.prepare(`SELECT 1 FROM ${quote(op.table.name)} WHERE json_extract(${quote(t.from)}, '$[0]') IS NULL LIMIT 1`).all().length > 0
|
|
59
|
+
}
|
|
60
|
+
if (!liveCols.has(c)) {
|
|
61
|
+
// The column isn't live under its NEW name yet — it arrives via a plain `rename_column` that runs
|
|
62
|
+
// first. Probe its OLD live name instead (the rename preserves data, so a NOT NULL target still
|
|
63
|
+
// fails on rows the old column left NULL). Only skip when even the source name isn't live.
|
|
64
|
+
const from = renames.get(`${op.table.name} ${c}`)
|
|
65
|
+
if (from && liveCols.has(from)) return db.prepare(`SELECT 1 FROM ${quote(op.table.name)} WHERE ${quote(from)} IS NULL LIMIT 1`).all().length > 0
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
return db.prepare(`SELECT 1 FROM ${quote(op.table.name)} WHERE ${quote(c)} IS NULL LIMIT 1`).all().length > 0
|
|
69
|
+
}
|
|
70
|
+
for (const c of op.table.columns) {
|
|
71
|
+
if (!c.notNull || c.default !== null || c.autoIncrement) continue
|
|
72
|
+
if (!carried.has(c.name)) {
|
|
73
|
+
problems.push(`${op.table.name}.${c.name}: new NOT NULL column has no default and no data to copy`)
|
|
74
|
+
} else if (liveSourceIsNull(c.name) === true) {
|
|
75
|
+
problems.push(`${op.table.name}.${c.name}: became NOT NULL but existing rows hold NULL`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// A UNIQUE index recreated by the rebuild over carried-over data that already holds duplicates would
|
|
79
|
+
// abort the migration; flag it up front. (SQLite treats NULLs as distinct, so only non-null rows count.)
|
|
80
|
+
// Only probe when every index column already exists live (a renamed/transformed column can't be checked
|
|
81
|
+
// here without a live name — skip rather than crash, same as the NULL probe above).
|
|
82
|
+
for (const idx of op.table.indexes) {
|
|
83
|
+
if (!idx.unique || !idx.columns.every((c) => carried.has(c) && liveCols.has(c))) continue
|
|
84
|
+
const cols = idx.columns.map(quote).join(', ')
|
|
85
|
+
const notNull = idx.columns.map((c) => `${quote(c)} IS NOT NULL`).join(' AND ')
|
|
86
|
+
const partial = idx.where ? ` AND (${idx.where})` : ''
|
|
87
|
+
const dup = db.prepare(`SELECT 1 FROM ${quote(op.table.name)} WHERE ${notNull}${partial} GROUP BY ${cols} HAVING COUNT(*) > 1 LIMIT 1`).all().length > 0
|
|
88
|
+
if (dup) problems.push(`${op.table.name}.(${idx.columns.join(', ')}): UNIQUE index "${idx.name}" has duplicate values in existing rows`)
|
|
89
|
+
}
|
|
90
|
+
return problems
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A plain `ALTER TABLE … ADD COLUMN` can't add a NOT NULL column with no default to a table that
|
|
95
|
+
* already holds rows (SQLite rejects it; an empty table is fine). This is reachable purely additively —
|
|
96
|
+
* adding a required field with no default to a populated collection emits a standalone `add_column`,
|
|
97
|
+
* never a rebuild — so the rebuild pre-flight misses it. Surface it as the same up-front 'infeasible'
|
|
98
|
+
* message instead of an opaque mid-transaction abort.
|
|
99
|
+
*/
|
|
100
|
+
function addColumnProblems(db: SyncDb, op: Extract<SchemaOp, { type: 'add_column' }>, quote: Dialect['quote']): string[] {
|
|
101
|
+
const c = op.column
|
|
102
|
+
if (!c.notNull || c.default !== null || c.autoIncrement) return []
|
|
103
|
+
const hasRows = db.prepare(`SELECT 1 FROM ${quote(op.table)} LIMIT 1`).all().length > 0
|
|
104
|
+
return hasRows ? [`${op.table}.${c.name}: new NOT NULL column has no default and the table already has rows`] : []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A new standalone UNIQUE index (e.g. flipping an existing populated column to `unique: true`) aborts if
|
|
109
|
+
* current rows already hold duplicate non-null values. Same duplicate probe `rebuildProblems` runs, but
|
|
110
|
+
* for a `create_index` op against the live table. Skip when an index column isn't live yet (it arrives
|
|
111
|
+
* via a rename that runs first) rather than probe a missing column and abort with `no such column`.
|
|
112
|
+
*/
|
|
113
|
+
function createIndexProblems(db: SyncDb, op: Extract<SchemaOp, { type: 'create_index' }>, quote: Dialect['quote']): string[] {
|
|
114
|
+
const idx = op.index
|
|
115
|
+
if (!idx.unique) return []
|
|
116
|
+
const liveCols = new Set((db.pragma(`table_info("${idx.table.replace(/"/g, '""')}")`) as { name: string }[]).map((r) => r.name))
|
|
117
|
+
if (!idx.columns.every((c) => liveCols.has(c))) return []
|
|
118
|
+
const cols = idx.columns.map(quote).join(', ')
|
|
119
|
+
const notNull = idx.columns.map((c) => `${quote(c)} IS NOT NULL`).join(' AND ')
|
|
120
|
+
const partial = idx.where ? ` AND (${idx.where})` : ''
|
|
121
|
+
const dup = db.prepare(`SELECT 1 FROM ${quote(idx.table)} WHERE ${notNull}${partial} GROUP BY ${cols} HAVING COUNT(*) > 1 LIMIT 1`).all().length > 0
|
|
122
|
+
return dup ? [`${idx.table}.(${idx.columns.join(', ')}): UNIQUE index "${idx.name}" has duplicate values in existing rows`] : []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Compute the ops needed to bring `db` to `desired` WITHOUT applying them — the dry-run / `--check`
|
|
126
|
+
* source. Reads the live schema through `dialect.introspect` (defaults to SQLite). */
|
|
127
|
+
export function planOps(db: SyncDb, desired: SchemaSnapshot, dialect: Dialect = sqlite): SchemaOp[] {
|
|
128
|
+
return diffSchema(desired, dialect.introspect(db))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SyncOptions {
|
|
132
|
+
/** Apply `rebuild_table` ops (a managed collection's columns were dropped or changed). */
|
|
133
|
+
allowDestructive?: boolean
|
|
134
|
+
/** Table names allowed to be dropped. A `drop_table` applies ONLY if its table is listed here, so an
|
|
135
|
+
* unmanaged / unrelated table is never dropped just because `allowDestructive` was set. */
|
|
136
|
+
dropTables?: string[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface SyncResult {
|
|
140
|
+
/** DDL statements applied, in order (empty when already in sync). */
|
|
141
|
+
applied: string[]
|
|
142
|
+
/** Destructive ops withheld (not opted into) — the DB was left intact for them. */
|
|
143
|
+
skipped: SchemaOp[]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reconcile a live database to `desired` through `dialect` (defaults to SQLite). Additive ops always
|
|
148
|
+
* apply. Destructive ops need an explicit, op-specific opt-in: `rebuild_table` requires
|
|
149
|
+
* `allowDestructive`; `drop_table` requires the table to be named in `dropTables` (a blanket
|
|
150
|
+
* `allowDestructive` never drops tables). Withheld ops are returned in `skipped`. Everything applied runs
|
|
151
|
+
* in one transaction.
|
|
152
|
+
*/
|
|
153
|
+
export function syncSchema(db: SyncDb, desired: SchemaSnapshot, opts: SyncOptions = {}, dialect: Dialect = sqlite): SyncResult {
|
|
154
|
+
const droppable = new Set(opts.dropTables ?? [])
|
|
155
|
+
const allowed = (op: SchemaOp): boolean => {
|
|
156
|
+
if (op.type === 'rebuild_table') return opts.allowDestructive === true
|
|
157
|
+
if (op.type === 'drop_table') return droppable.has(op.name)
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
const ops = planOps(db, desired, dialect)
|
|
161
|
+
let toApply = ops.filter(allowed)
|
|
162
|
+
let skipped = ops.filter((op) => !allowed(op))
|
|
163
|
+
|
|
164
|
+
// A `rename_column` queued ahead of a `rebuild_table` is only sound as that rebuild's prologue (diff.ts
|
|
165
|
+
// never re-gates it on the rebuild being applied). If the rebuild was withheld, ship neither half — a
|
|
166
|
+
// bare rename would leave the table renamed but not rebuilt, silently diverging from `desired`.
|
|
167
|
+
const withheldRebuildTables = new Set(skipped.filter((op) => op.type === 'rebuild_table').map((op) => op.table.name))
|
|
168
|
+
if (withheldRebuildTables.size) {
|
|
169
|
+
const demoted = toApply.filter((op) => op.type === 'rename_column' && withheldRebuildTables.has(op.table))
|
|
170
|
+
if (demoted.length) {
|
|
171
|
+
toApply = toApply.filter((op) => !demoted.includes(op))
|
|
172
|
+
skipped = [...skipped, ...demoted]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validate feasibility BEFORE opening the transaction, so an impossible migration fails loud and
|
|
177
|
+
// changes nothing (rather than aborting the transaction with an opaque constraint error). Every
|
|
178
|
+
// apply-time-fatal op is pre-flighted: rebuilds, plus the purely-additive add_column(NOT NULL, no
|
|
179
|
+
// default) on a populated table and create_index(UNIQUE) over existing duplicates.
|
|
180
|
+
// Plain renames (`ALTER TABLE … RENAME COLUMN`) run BEFORE the rebuilds, so a rebuild's carried column
|
|
181
|
+
// may be probed under its OLD live name — build a PER-TABLE new→old map so the NOT NULL feasibility check
|
|
182
|
+
// can. Keyed by table: two tables renaming a different column to the SAME new name must not collide.
|
|
183
|
+
const renames = new Map<string, string>() // `${table} ${to}` → from
|
|
184
|
+
for (const op of toApply) if (op.type === 'rename_column') renames.set(`${op.table} ${op.to}`, op.from)
|
|
185
|
+
const problems = toApply.flatMap((op) =>
|
|
186
|
+
op.type === 'rebuild_table' ? rebuildProblems(db, op, dialect.quote, renames)
|
|
187
|
+
: op.type === 'add_column' ? addColumnProblems(db, op, dialect.quote)
|
|
188
|
+
: op.type === 'create_index' ? createIndexProblems(db, op, dialect.quote)
|
|
189
|
+
: [])
|
|
190
|
+
if (problems.length) {
|
|
191
|
+
throw new Error(`kestrel: schema migration is infeasible — fix the collection or its data, then retry:\n - ${problems.join('\n - ')}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const applied = dialect.render(toApply)
|
|
195
|
+
if (applied.length) {
|
|
196
|
+
db.transaction(() => {
|
|
197
|
+
for (const sql of applied) db.exec(sql)
|
|
198
|
+
})()
|
|
199
|
+
}
|
|
200
|
+
return { applied, skipped }
|
|
201
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import collections from '#kestrel/collections'
|
|
2
|
+
import schemaTables from '#kestrel/schema-tables'
|
|
3
|
+
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
4
|
+
import type { BuiltCollection } from '../../utils/collection-types'
|
|
5
|
+
import type { CollectionDef } from '../../utils/defineCollection'
|
|
6
|
+
import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
|
|
7
|
+
import { desiredFromCollections } from '../../schema/bootstrap'
|
|
8
|
+
import { planOps, syncSchema, isDestructive, describeOp, type SyncDb } from '../../schema/sync'
|
|
9
|
+
import { sqlite } from '../../schema/dialect'
|
|
10
|
+
|
|
11
|
+
// Explicit schema-migrate step for production (ADR-0002). Dev auto-syncs additively at boot
|
|
12
|
+
// (02.schema-sync); prod boot never auto-DDLs, so an operator runs this task. Triggering (Nuxt 4.4 /
|
|
13
|
+
// Nitro 2.13 — there is NO `nuxi task run`):
|
|
14
|
+
// - dev: GET http://localhost:3000/_nitro/tasks/db:migrate (the dev-only task route)
|
|
15
|
+
// - prod: call runTask('db:migrate', { payload }) from inside the process (an authenticated route or a
|
|
16
|
+
// cron scheduledTask); the built node-server has no task CLI/endpoint.
|
|
17
|
+
// Payloads: {check:true}=dry-run (report pending DDL, change nothing); {force:true}=also apply REBUILDS
|
|
18
|
+
// (column drop/type change); {drop:["old_table"]}=drop a specific table (never bundled in force).
|
|
19
|
+
// Destructive ops are reported-but-withheld unless explicitly opted into; `force` never drops a table.
|
|
20
|
+
// The two run() branches return different result shapes (dry-run report vs applied/skipped); the explicit
|
|
21
|
+
// union is the task's result type so neither branch is forced to match the other.
|
|
22
|
+
type MigrateResult =
|
|
23
|
+
| { check: boolean; pending: string[]; destructive: string[] }
|
|
24
|
+
| { applied: string[]; skipped: string[] }
|
|
25
|
+
|
|
26
|
+
export default defineTask<MigrateResult>({
|
|
27
|
+
meta: { name: 'db:migrate', description: 'Apply schema changes; {check:true}=dry-run, {force:true}=rebuilds, {drop:[names]}=drop tables' },
|
|
28
|
+
run({ payload }) {
|
|
29
|
+
const p = (payload ?? {}) as { check?: boolean; force?: boolean; drop?: string[] }
|
|
30
|
+
const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
|
|
31
|
+
// Same toggle source as the registry plugin, so runtime surface and schema agree (see 02.schema-sync).
|
|
32
|
+
toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
|
|
33
|
+
extraTables: schemaTables as AnySQLiteTable[],
|
|
34
|
+
})
|
|
35
|
+
const client = (useDb() as unknown as { $client: SyncDb }).$client
|
|
36
|
+
// SQLite is the only backend today; this is the seam where a config-driven `resolveDialect()` would go.
|
|
37
|
+
const dialect = sqlite
|
|
38
|
+
|
|
39
|
+
if (p.check === true) {
|
|
40
|
+
const ops = planOps(client, desired, dialect)
|
|
41
|
+
const destructive = ops.filter(isDestructive).map(describeOp)
|
|
42
|
+
console.info(`[kestrel] db:migrate dry-run: ${ops.length} pending op(s), ${destructive.length} destructive`)
|
|
43
|
+
return { result: { check: true, pending: dialect.render(ops), destructive } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { applied, skipped } = syncSchema(client, desired, {
|
|
47
|
+
allowDestructive: p.force === true,
|
|
48
|
+
dropTables: Array.isArray(p.drop) ? p.drop : undefined,
|
|
49
|
+
}, dialect)
|
|
50
|
+
if (skipped.length) console.warn(`[kestrel] db:migrate withheld ${skipped.length} destructive change(s) — opt in with {"force":true} (rebuilds) / {"drop":[…]} (drops):\n - ${skipped.map(describeOp).join('\n - ')}`)
|
|
51
|
+
console.info(`[kestrel] db:migrate applied ${applied.length} statement(s)`)
|
|
52
|
+
return { result: { applied, skipped: skipped.map(describeOp) } }
|
|
53
|
+
},
|
|
54
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
import type { CollectionDef } from './defineCollection'
|
|
4
|
+
|
|
5
|
+
/** The validation schemas are loose `ZodObject`s. drizzle-zod's builders are overloaded such that
|
|
6
|
+
* `ReturnType<typeof createInsertSchema>` resolves to the WRONG (ZodEnum) overload, so the shape is stated
|
|
7
|
+
* directly — `ZodObject` carries the `.safeParse()` / `.partial()` the CRUD layer calls; `buildCollection`
|
|
8
|
+
* casts the builder results to it. */
|
|
9
|
+
export type CollectionSchema = z.ZodObject<z.ZodRawShape>
|
|
10
|
+
|
|
11
|
+
export interface ConditionIssue {
|
|
12
|
+
path: (string | number)[]
|
|
13
|
+
message: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BuiltCollection {
|
|
17
|
+
name: string
|
|
18
|
+
def: CollectionDef
|
|
19
|
+
table: SQLiteTable
|
|
20
|
+
insert: CollectionSchema
|
|
21
|
+
update: CollectionSchema
|
|
22
|
+
select: CollectionSchema
|
|
23
|
+
/** Re-enforce `required` for conditional fields whose condition is met against the whole record
|
|
24
|
+
* (the per-field schema can't see siblings). Returns Zod-shaped issues keyed by the field's def
|
|
25
|
+
* name. Present only when the collection has conditional required fields. */
|
|
26
|
+
applyConditions?: (record: Record<string, unknown>) => { issues: ConditionIssue[] }
|
|
27
|
+
}
|