@karibsen/eponyme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +609 -0
- package/dist/eponyme.d.mts +15 -0
- package/dist/eponyme.mjs +38 -0
- package/dist/module.d.mts +26 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +154 -0
- package/dist/runtime/assets/dashboard.css +1 -0
- package/dist/runtime/components/editor/EponymeCollectionPage.d.vue.ts +9 -0
- package/dist/runtime/components/editor/EponymeCollectionPage.vue +238 -0
- package/dist/runtime/components/editor/EponymeCollectionPage.vue.d.ts +9 -0
- package/dist/runtime/components/editor/EponymeCopy.d.vue.ts +7 -0
- package/dist/runtime/components/editor/EponymeCopy.vue +28 -0
- package/dist/runtime/components/editor/EponymeCopy.vue.d.ts +7 -0
- package/dist/runtime/components/editor/EponymeFolderNav.d.vue.ts +7 -0
- package/dist/runtime/components/editor/EponymeFolderNav.vue +146 -0
- package/dist/runtime/components/editor/EponymeFolderNav.vue.d.ts +7 -0
- package/dist/runtime/components/editor/EponymeFolderPage.d.vue.ts +7 -0
- package/dist/runtime/components/editor/EponymeFolderPage.vue +62 -0
- package/dist/runtime/components/editor/EponymeFolderPage.vue.d.ts +7 -0
- package/dist/runtime/components/editor/EponymeForm.d.vue.ts +9 -0
- package/dist/runtime/components/editor/EponymeForm.vue +309 -0
- package/dist/runtime/components/editor/EponymeForm.vue.d.ts +9 -0
- package/dist/runtime/components/editor/EponymeFormPage.d.vue.ts +8 -0
- package/dist/runtime/components/editor/EponymeFormPage.vue +288 -0
- package/dist/runtime/components/editor/EponymeFormPage.vue.d.ts +8 -0
- package/dist/runtime/components/editor/EponymeHistoryDialog.d.vue.ts +14 -0
- package/dist/runtime/components/editor/EponymeHistoryDialog.vue +97 -0
- package/dist/runtime/components/editor/EponymeHistoryDialog.vue.d.ts +14 -0
- package/dist/runtime/components/editor/EponymeNavigationLink.d.vue.ts +14 -0
- package/dist/runtime/components/editor/EponymeNavigationLink.vue +80 -0
- package/dist/runtime/components/editor/EponymeNavigationLink.vue.d.ts +14 -0
- package/dist/runtime/components/editor/EponymePageNavigation.d.vue.ts +8 -0
- package/dist/runtime/components/editor/EponymePageNavigation.vue +58 -0
- package/dist/runtime/components/editor/EponymePageNavigation.vue.d.ts +8 -0
- package/dist/runtime/components/editor/EponymePreviewPanel.d.vue.ts +16 -0
- package/dist/runtime/components/editor/EponymePreviewPanel.vue +100 -0
- package/dist/runtime/components/editor/EponymePreviewPanel.vue.d.ts +16 -0
- package/dist/runtime/components/editor/EponymeSidebar.d.vue.ts +6 -0
- package/dist/runtime/components/editor/EponymeSidebar.vue +244 -0
- package/dist/runtime/components/editor/EponymeSidebar.vue.d.ts +6 -0
- package/dist/runtime/components/editor/EponymeSidebarTree.d.vue.ts +18 -0
- package/dist/runtime/components/editor/EponymeSidebarTree.vue +84 -0
- package/dist/runtime/components/editor/EponymeSidebarTree.vue.d.ts +18 -0
- package/dist/runtime/components/fields/ArrayField.d.vue.ts +19 -0
- package/dist/runtime/components/fields/ArrayField.vue +198 -0
- package/dist/runtime/components/fields/ArrayField.vue.d.ts +19 -0
- package/dist/runtime/components/fields/BooleanField.d.vue.ts +16 -0
- package/dist/runtime/components/fields/BooleanField.vue +36 -0
- package/dist/runtime/components/fields/BooleanField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/CheckboxGroupField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/CheckboxGroupField.vue +52 -0
- package/dist/runtime/components/fields/CheckboxGroupField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/ColorField.d.vue.ts +19 -0
- package/dist/runtime/components/fields/ColorField.vue +39 -0
- package/dist/runtime/components/fields/ColorField.vue.d.ts +19 -0
- package/dist/runtime/components/fields/DateField.d.vue.ts +18 -0
- package/dist/runtime/components/fields/DateField.vue +39 -0
- package/dist/runtime/components/fields/DateField.vue.d.ts +18 -0
- package/dist/runtime/components/fields/FieldRenderer.d.vue.ts +24 -0
- package/dist/runtime/components/fields/FieldRenderer.vue +135 -0
- package/dist/runtime/components/fields/FieldRenderer.vue.d.ts +24 -0
- package/dist/runtime/components/fields/NumberField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/NumberField.vue +58 -0
- package/dist/runtime/components/fields/NumberField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/RadioField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/RadioField.vue +44 -0
- package/dist/runtime/components/fields/RadioField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/RichTextField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/RichTextField.vue +176 -0
- package/dist/runtime/components/fields/RichTextField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/SectionField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/SectionField.vue +127 -0
- package/dist/runtime/components/fields/SectionField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/SelectField.d.vue.ts +21 -0
- package/dist/runtime/components/fields/SelectField.vue +42 -0
- package/dist/runtime/components/fields/SelectField.vue.d.ts +21 -0
- package/dist/runtime/components/fields/SlugField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/SlugField.vue +35 -0
- package/dist/runtime/components/fields/SlugField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/TabsField.d.vue.ts +19 -0
- package/dist/runtime/components/fields/TabsField.vue +109 -0
- package/dist/runtime/components/fields/TabsField.vue.d.ts +19 -0
- package/dist/runtime/components/fields/TextField.d.vue.ts +22 -0
- package/dist/runtime/components/fields/TextField.vue +56 -0
- package/dist/runtime/components/fields/TextField.vue.d.ts +22 -0
- package/dist/runtime/components/fields/TextareaField.d.vue.ts +20 -0
- package/dist/runtime/components/fields/TextareaField.vue +47 -0
- package/dist/runtime/components/fields/TextareaField.vue.d.ts +20 -0
- package/dist/runtime/components/fields/UrlField.d.vue.ts +17 -0
- package/dist/runtime/components/fields/UrlField.vue +35 -0
- package/dist/runtime/components/fields/UrlField.vue.d.ts +17 -0
- package/dist/runtime/components/ui/EPAccordion.d.vue.ts +26 -0
- package/dist/runtime/components/ui/EPAccordion.vue +64 -0
- package/dist/runtime/components/ui/EPAccordion.vue.d.ts +26 -0
- package/dist/runtime/components/ui/EPAvatar.d.vue.ts +9 -0
- package/dist/runtime/components/ui/EPAvatar.vue +25 -0
- package/dist/runtime/components/ui/EPAvatar.vue.d.ts +9 -0
- package/dist/runtime/components/ui/EPBadge.d.vue.ts +16 -0
- package/dist/runtime/components/ui/EPBadge.vue +17 -0
- package/dist/runtime/components/ui/EPBadge.vue.d.ts +16 -0
- package/dist/runtime/components/ui/EPButton.d.vue.ts +22 -0
- package/dist/runtime/components/ui/EPButton.vue +55 -0
- package/dist/runtime/components/ui/EPButton.vue.d.ts +22 -0
- package/dist/runtime/components/ui/EPCheckbox.d.vue.ts +11 -0
- package/dist/runtime/components/ui/EPCheckbox.vue +21 -0
- package/dist/runtime/components/ui/EPCheckbox.vue.d.ts +11 -0
- package/dist/runtime/components/ui/EPColorPicker.d.vue.ts +19 -0
- package/dist/runtime/components/ui/EPColorPicker.vue +63 -0
- package/dist/runtime/components/ui/EPColorPicker.vue.d.ts +19 -0
- package/dist/runtime/components/ui/EPDialog.d.vue.ts +24 -0
- package/dist/runtime/components/ui/EPDialog.vue +54 -0
- package/dist/runtime/components/ui/EPDialog.vue.d.ts +24 -0
- package/dist/runtime/components/ui/EPDropdownMenu.d.vue.ts +25 -0
- package/dist/runtime/components/ui/EPDropdownMenu.vue +40 -0
- package/dist/runtime/components/ui/EPDropdownMenu.vue.d.ts +25 -0
- package/dist/runtime/components/ui/EPFormField.d.vue.ts +24 -0
- package/dist/runtime/components/ui/EPFormField.vue +62 -0
- package/dist/runtime/components/ui/EPFormField.vue.d.ts +24 -0
- package/dist/runtime/components/ui/EPInputText.d.vue.ts +17 -0
- package/dist/runtime/components/ui/EPInputText.vue +33 -0
- package/dist/runtime/components/ui/EPInputText.vue.d.ts +17 -0
- package/dist/runtime/components/ui/EPLinkEditor.d.vue.ts +16 -0
- package/dist/runtime/components/ui/EPLinkEditor.vue +169 -0
- package/dist/runtime/components/ui/EPLinkEditor.vue.d.ts +16 -0
- package/dist/runtime/components/ui/EPRadioButton.d.vue.ts +16 -0
- package/dist/runtime/components/ui/EPRadioButton.vue +34 -0
- package/dist/runtime/components/ui/EPRadioButton.vue.d.ts +16 -0
- package/dist/runtime/components/ui/EPSelect.d.vue.ts +21 -0
- package/dist/runtime/components/ui/EPSelect.vue +73 -0
- package/dist/runtime/components/ui/EPSelect.vue.d.ts +21 -0
- package/dist/runtime/components/ui/EPSlider.d.vue.ts +16 -0
- package/dist/runtime/components/ui/EPSlider.vue +51 -0
- package/dist/runtime/components/ui/EPSlider.vue.d.ts +16 -0
- package/dist/runtime/components/ui/EPSwitch.d.vue.ts +11 -0
- package/dist/runtime/components/ui/EPSwitch.vue +23 -0
- package/dist/runtime/components/ui/EPSwitch.vue.d.ts +11 -0
- package/dist/runtime/components/ui/EPTextarea.d.vue.ts +12 -0
- package/dist/runtime/components/ui/EPTextarea.vue +18 -0
- package/dist/runtime/components/ui/EPTextarea.vue.d.ts +12 -0
- package/dist/runtime/components/ui/EPToast.d.vue.ts +13 -0
- package/dist/runtime/components/ui/EPToast.vue +60 -0
- package/dist/runtime/components/ui/EPToast.vue.d.ts +13 -0
- package/dist/runtime/components/ui/EPTooltip.d.vue.ts +17 -0
- package/dist/runtime/components/ui/EPTooltip.vue +34 -0
- package/dist/runtime/components/ui/EPTooltip.vue.d.ts +17 -0
- package/dist/runtime/components/ui/form-field-context.d.ts +12 -0
- package/dist/runtime/components/ui/form-field-context.js +1 -0
- package/dist/runtime/composables/useEponyme.d.ts +24 -0
- package/dist/runtime/composables/useEponyme.js +63 -0
- package/dist/runtime/composables/useEponymeAuth.d.ts +16 -0
- package/dist/runtime/composables/useEponymeAuth.js +81 -0
- package/dist/runtime/composables/useEponymeCollection.d.ts +27 -0
- package/dist/runtime/composables/useEponymeCollection.js +44 -0
- package/dist/runtime/composables/useEponymeColorPresets.d.ts +5 -0
- package/dist/runtime/composables/useEponymeColorPresets.js +16 -0
- package/dist/runtime/composables/useEponymeConfig.d.ts +1 -0
- package/dist/runtime/composables/useEponymeConfig.js +4 -0
- package/dist/runtime/composables/useEponymeForm.d.ts +35 -0
- package/dist/runtime/composables/useEponymeForm.js +75 -0
- package/dist/runtime/composables/useEponymeHistory.d.ts +26 -0
- package/dist/runtime/composables/useEponymeHistory.js +24 -0
- package/dist/runtime/composables/useEponymeValidation.d.ts +21 -0
- package/dist/runtime/composables/useEponymeValidation.js +34 -0
- package/dist/runtime/fields/array.d.ts +2 -0
- package/dist/runtime/fields/array.js +3 -0
- package/dist/runtime/fields/boolean.d.ts +2 -0
- package/dist/runtime/fields/boolean.js +3 -0
- package/dist/runtime/fields/checkbox-group.d.ts +2 -0
- package/dist/runtime/fields/checkbox-group.js +3 -0
- package/dist/runtime/fields/color.d.ts +2 -0
- package/dist/runtime/fields/color.js +3 -0
- package/dist/runtime/fields/date.d.ts +3 -0
- package/dist/runtime/fields/date.js +6 -0
- package/dist/runtime/fields/email.d.ts +2 -0
- package/dist/runtime/fields/email.js +3 -0
- package/dist/runtime/fields/image.d.ts +2 -0
- package/dist/runtime/fields/image.js +3 -0
- package/dist/runtime/fields/index.d.ts +37 -0
- package/dist/runtime/fields/index.js +19 -0
- package/dist/runtime/fields/number.d.ts +2 -0
- package/dist/runtime/fields/number.js +3 -0
- package/dist/runtime/fields/radio.d.ts +2 -0
- package/dist/runtime/fields/radio.js +3 -0
- package/dist/runtime/fields/rich-text.d.ts +2 -0
- package/dist/runtime/fields/rich-text.js +3 -0
- package/dist/runtime/fields/section.d.ts +2 -0
- package/dist/runtime/fields/section.js +3 -0
- package/dist/runtime/fields/select.d.ts +2 -0
- package/dist/runtime/fields/select.js +3 -0
- package/dist/runtime/fields/slug.d.ts +2 -0
- package/dist/runtime/fields/slug.js +3 -0
- package/dist/runtime/fields/string.d.ts +2 -0
- package/dist/runtime/fields/string.js +3 -0
- package/dist/runtime/fields/tab.d.ts +2 -0
- package/dist/runtime/fields/tab.js +3 -0
- package/dist/runtime/fields/textarea.d.ts +2 -0
- package/dist/runtime/fields/textarea.js +3 -0
- package/dist/runtime/fields/url.d.ts +2 -0
- package/dist/runtime/fields/url.js +3 -0
- package/dist/runtime/middleware/eponyme-auth.d.ts +2 -0
- package/dist/runtime/middleware/eponyme-auth.js +13 -0
- package/dist/runtime/middleware/eponyme-owner.d.ts +2 -0
- package/dist/runtime/middleware/eponyme-owner.js +7 -0
- package/dist/runtime/pages/EponymeChangePasswordPage.d.vue.ts +3 -0
- package/dist/runtime/pages/EponymeChangePasswordPage.vue +118 -0
- package/dist/runtime/pages/EponymeChangePasswordPage.vue.d.ts +3 -0
- package/dist/runtime/pages/EponymeDetailPage.d.vue.ts +3 -0
- package/dist/runtime/pages/EponymeDetailPage.vue +83 -0
- package/dist/runtime/pages/EponymeDetailPage.vue.d.ts +3 -0
- package/dist/runtime/pages/EponymeIndexPage.d.vue.ts +3 -0
- package/dist/runtime/pages/EponymeIndexPage.vue +51 -0
- package/dist/runtime/pages/EponymeIndexPage.vue.d.ts +3 -0
- package/dist/runtime/pages/EponymeLoginPage.d.vue.ts +3 -0
- package/dist/runtime/pages/EponymeLoginPage.vue +102 -0
- package/dist/runtime/pages/EponymeLoginPage.vue.d.ts +3 -0
- package/dist/runtime/pages/EponymeUsersPage.d.vue.ts +3 -0
- package/dist/runtime/pages/EponymeUsersPage.vue +261 -0
- package/dist/runtime/pages/EponymeUsersPage.vue.d.ts +3 -0
- package/dist/runtime/server/api/eponyme/[name].get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme/[name].get.js +20 -0
- package/dist/runtime/server/api/eponyme/[name].patch.d.ts +11 -0
- package/dist/runtime/server/api/eponyme/[name].patch.js +20 -0
- package/dist/runtime/server/api/eponyme-auth/change-password.post.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-auth/change-password.post.js +21 -0
- package/dist/runtime/server/api/eponyme-auth/login.post.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-auth/login.post.js +12 -0
- package/dist/runtime/server/api/eponyme-auth/logout.post.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-auth/logout.post.js +13 -0
- package/dist/runtime/server/api/eponyme-auth/session.get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-auth/session.get.js +5 -0
- package/dist/runtime/server/api/eponyme-collections/[path].delete.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-collections/[path].delete.js +11 -0
- package/dist/runtime/server/api/eponyme-collections/[path].get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-collections/[path].get.js +11 -0
- package/dist/runtime/server/api/eponyme-collections/[path].post.d.ts +7 -0
- package/dist/runtime/server/api/eponyme-collections/[path].post.js +16 -0
- package/dist/runtime/server/api/eponyme-forms/[path].delete.d.ts +6 -0
- package/dist/runtime/server/api/eponyme-forms/[path].delete.js +19 -0
- package/dist/runtime/server/api/eponyme-forms/[path].get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-forms/[path].get.js +22 -0
- package/dist/runtime/server/api/eponyme-forms/[path].post.d.ts +19 -0
- package/dist/runtime/server/api/eponyme-forms/[path].post.js +36 -0
- package/dist/runtime/server/api/eponyme-history/[path].get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-history/[path].get.js +11 -0
- package/dist/runtime/server/api/eponyme-history/[path].patch.d.ts +2 -0
- package/dist/runtime/server/api/eponyme-history/[path].patch.js +18 -0
- package/dist/runtime/server/api/eponyme-sitemap.get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-sitemap.get.js +5 -0
- package/dist/runtime/server/api/eponyme-statuses.get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-statuses.get.js +7 -0
- package/dist/runtime/server/api/eponyme-users/[id]/reset-password.post.d.ts +5 -0
- package/dist/runtime/server/api/eponyme-users/[id]/reset-password.post.js +12 -0
- package/dist/runtime/server/api/eponyme-users/[id].patch.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-users/[id].patch.js +14 -0
- package/dist/runtime/server/api/eponyme-users/index.get.d.ts +4 -0
- package/dist/runtime/server/api/eponyme-users/index.get.js +7 -0
- package/dist/runtime/server/api/eponyme-users/index.post.d.ts +5 -0
- package/dist/runtime/server/api/eponyme-users/index.post.js +13 -0
- package/dist/runtime/server/plugins/eponyme-sync.d.ts +2 -0
- package/dist/runtime/server/plugins/eponyme-sync.js +14 -0
- package/dist/runtime/server/services/eponyme-auth-service.d.ts +2 -0
- package/dist/runtime/server/services/eponyme-auth-service.js +15 -0
- package/dist/runtime/server/services/eponyme-auth-store.d.ts +119 -0
- package/dist/runtime/server/services/eponyme-auth-store.js +227 -0
- package/dist/runtime/server/services/eponyme-form-service.d.ts +3 -0
- package/dist/runtime/server/services/eponyme-form-service.js +8 -0
- package/dist/runtime/server/services/eponyme-form-store.d.ts +100 -0
- package/dist/runtime/server/services/eponyme-form-store.js +109 -0
- package/dist/runtime/server/services/eponyme-service.d.ts +3 -0
- package/dist/runtime/server/services/eponyme-service.js +8 -0
- package/dist/runtime/server/services/eponyme-store.d.ts +202 -0
- package/dist/runtime/server/services/eponyme-store.js +338 -0
- package/dist/runtime/server/utils/auth.d.ts +11 -0
- package/dist/runtime/server/utils/auth.js +41 -0
- package/dist/runtime/server/utils/eponyme-form.d.ts +14 -0
- package/dist/runtime/server/utils/eponyme-form.js +6 -0
- package/dist/runtime/server/utils/eponyme-sitemap.d.ts +7 -0
- package/dist/runtime/server/utils/eponyme-sitemap.js +7 -0
- package/dist/runtime/server/utils/form-route.d.ts +14 -0
- package/dist/runtime/server/utils/form-route.js +14 -0
- package/dist/runtime/server/utils/password.d.ts +4 -0
- package/dist/runtime/server/utils/password.js +30 -0
- package/dist/runtime/types/auth.d.ts +20 -0
- package/dist/runtime/types/auth.js +7 -0
- package/dist/runtime/types/eponyme-config.d.ts +11 -0
- package/dist/runtime/types/eponyme-navigation.d.ts +6 -0
- package/dist/runtime/types/eponyme-navigation.js +0 -0
- package/dist/runtime/types/eponyme.d.ts +87 -0
- package/dist/runtime/types/eponyme.js +0 -0
- package/dist/runtime/types/field.d.ts +203 -0
- package/dist/runtime/types/field.js +0 -0
- package/dist/runtime/types/index.d.ts +3 -0
- package/dist/runtime/types/index.js +3 -0
- package/dist/runtime/types/nuxt.d.ts +11 -0
- package/dist/runtime/utils/as-record.d.ts +1 -0
- package/dist/runtime/utils/as-record.js +3 -0
- package/dist/runtime/utils/build-eponyme-schema.d.ts +22 -0
- package/dist/runtime/utils/build-eponyme-schema.js +42 -0
- package/dist/runtime/utils/changed-paths.d.ts +6 -0
- package/dist/runtime/utils/changed-paths.js +25 -0
- package/dist/runtime/utils/create-default-eponyme-data.d.ts +5 -0
- package/dist/runtime/utils/create-default-eponyme-data.js +12 -0
- package/dist/runtime/utils/field-path.d.ts +14 -0
- package/dist/runtime/utils/field-path.js +23 -0
- package/dist/runtime/utils/get-eponyme-schemas.d.ts +7 -0
- package/dist/runtime/utils/get-eponyme-schemas.js +33 -0
- package/dist/runtime/utils/get-field-default-value.d.ts +4 -0
- package/dist/runtime/utils/get-field-default-value.js +56 -0
- package/dist/runtime/utils/humanize-label.d.ts +6 -0
- package/dist/runtime/utils/humanize-label.js +3 -0
- package/dist/runtime/utils/initials.d.ts +1 -0
- package/dist/runtime/utils/initials.js +3 -0
- package/dist/runtime/utils/is-field-visible.d.ts +2 -0
- package/dist/runtime/utils/is-field-visible.js +14 -0
- package/dist/runtime/utils/normalize-hex-color.d.ts +8 -0
- package/dist/runtime/utils/normalize-hex-color.js +11 -0
- package/dist/runtime/utils/normalize-slug.d.ts +1 -0
- package/dist/runtime/utils/normalize-slug.js +4 -0
- package/dist/runtime/utils/preview.d.ts +30 -0
- package/dist/runtime/utils/preview.js +30 -0
- package/dist/runtime/utils/validate-eponyme-data.d.ts +10 -0
- package/dist/runtime/utils/validate-eponyme-data.js +204 -0
- package/dist/types.d.mts +11 -0
- package/package.json +96 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { isEponymeRole } from "../../types/index.js";
|
|
3
|
+
import { generateTemporaryPassword, hashPassword, validatePassword, verifyPassword } from "../utils/password.js";
|
|
4
|
+
const OWNER_USERNAME = "EponymeOwner";
|
|
5
|
+
const MAX_LOGIN_ATTEMPTS = 5;
|
|
6
|
+
const LOGIN_LOCK_MS = 15 * 60 * 1e3;
|
|
7
|
+
const USERNAME_PATTERN = /^[\w.-]{3,50}$/;
|
|
8
|
+
const dummyPasswordHash = hashPassword("Eponyme timing-safe dummy password");
|
|
9
|
+
export class EponymeAuthService {
|
|
10
|
+
constructor(client, sessionDurationDays = 7) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.sessionDurationDays = sessionDurationDays;
|
|
13
|
+
}
|
|
14
|
+
async bootstrapOwner(log = console.log) {
|
|
15
|
+
if (await this.client.eponymeUser.count() > 0) return false;
|
|
16
|
+
const temporaryPassword = generateTemporaryPassword();
|
|
17
|
+
try {
|
|
18
|
+
await this.client.eponymeUser.create({
|
|
19
|
+
data: {
|
|
20
|
+
id: randomUUID(),
|
|
21
|
+
username: OWNER_USERNAME,
|
|
22
|
+
usernameNormalized: normalizeUsername(OWNER_USERNAME),
|
|
23
|
+
passwordHash: await hashPassword(temporaryPassword),
|
|
24
|
+
role: "owner",
|
|
25
|
+
active: true,
|
|
26
|
+
mustChangePassword: true,
|
|
27
|
+
failedLoginAttempts: 0
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (await this.client.eponymeUser.count() > 0) return false;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
log("[Eponyme] Initial owner account created. These credentials are shown only once.");
|
|
35
|
+
log(`[Eponyme] Username: ${OWNER_USERNAME}`);
|
|
36
|
+
log(`[Eponyme] Temporary password: ${temporaryPassword}`);
|
|
37
|
+
log("[Eponyme] Sign in and change this password before using the dashboard.");
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
async login(username, password) {
|
|
41
|
+
const normalized = typeof username === "string" ? normalizeUsername(username) : "";
|
|
42
|
+
const submittedPassword = typeof password === "string" ? password : "";
|
|
43
|
+
const user = normalized ? await this.client.eponymeUser.findUnique({ where: { usernameNormalized: normalized } }) : null;
|
|
44
|
+
const validPassword = await verifyPassword(submittedPassword, user?.passwordHash ?? await dummyPasswordHash);
|
|
45
|
+
if (!user || !user.active || !validPassword) {
|
|
46
|
+
if (user?.active) await this.recordFailedLogin(user);
|
|
47
|
+
return { ok: false, reason: "invalid" };
|
|
48
|
+
}
|
|
49
|
+
if (user.lockedUntil && new Date(user.lockedUntil).getTime() > Date.now())
|
|
50
|
+
return { ok: false, reason: "locked" };
|
|
51
|
+
if (user.failedLoginAttempts || user.lockedUntil) {
|
|
52
|
+
await this.client.eponymeUser.update({
|
|
53
|
+
where: { id: user.id },
|
|
54
|
+
data: { failedLoginAttempts: 0, lockedUntil: null }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, session: await this.createSession({ ...user, failedLoginAttempts: 0, lockedUntil: null }) };
|
|
58
|
+
}
|
|
59
|
+
async getSession(token) {
|
|
60
|
+
if (!token) return void 0;
|
|
61
|
+
const tokenHash = hashToken(token);
|
|
62
|
+
const session = await this.client.eponymeUserSession.findUnique({
|
|
63
|
+
where: { tokenHash },
|
|
64
|
+
include: { user: true }
|
|
65
|
+
});
|
|
66
|
+
if (!session?.user) return void 0;
|
|
67
|
+
if (new Date(session.expiresAt).getTime() <= Date.now() || !session.user.active) {
|
|
68
|
+
await this.client.eponymeUserSession.deleteMany({ where: { tokenHash } });
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
token,
|
|
73
|
+
expiresAt: new Date(session.expiresAt),
|
|
74
|
+
user: toAuthUser(session.user)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async logout(token) {
|
|
78
|
+
if (!token) return;
|
|
79
|
+
await this.client.eponymeUserSession.deleteMany({ where: { tokenHash: hashToken(token) } });
|
|
80
|
+
}
|
|
81
|
+
async changePassword(userId, currentPassword, newPassword) {
|
|
82
|
+
const user = await this.client.eponymeUser.findUnique({ where: { id: userId } });
|
|
83
|
+
if (!user || !user.active) return { error: "User account is unavailable." };
|
|
84
|
+
if (typeof currentPassword !== "string" || !await verifyPassword(currentPassword, user.passwordHash))
|
|
85
|
+
return { error: "Current password is incorrect." };
|
|
86
|
+
const passwordError = validatePassword(newPassword);
|
|
87
|
+
if (passwordError) return { error: passwordError };
|
|
88
|
+
if (normalizeUsername(String(newPassword)).includes(user.usernameNormalized))
|
|
89
|
+
return { error: "Password must not contain the username." };
|
|
90
|
+
if (await verifyPassword(String(newPassword), user.passwordHash))
|
|
91
|
+
return { error: "New password must be different from the current password." };
|
|
92
|
+
const updated = await this.client.eponymeUser.update({
|
|
93
|
+
where: { id: userId },
|
|
94
|
+
data: {
|
|
95
|
+
passwordHash: await hashPassword(String(newPassword)),
|
|
96
|
+
mustChangePassword: false,
|
|
97
|
+
failedLoginAttempts: 0,
|
|
98
|
+
lockedUntil: null
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await this.client.eponymeUserSession.deleteMany({ where: { userId } });
|
|
102
|
+
return { session: await this.createSession(updated) };
|
|
103
|
+
}
|
|
104
|
+
async listUsers() {
|
|
105
|
+
const users = await this.client.eponymeUser.findMany({ orderBy: { createdAt: "asc" } });
|
|
106
|
+
return users.map(toAuthUser);
|
|
107
|
+
}
|
|
108
|
+
async createUser(username, role) {
|
|
109
|
+
const usernameError = validateUsername(username);
|
|
110
|
+
if (usernameError) return { error: usernameError };
|
|
111
|
+
if (!isEponymeRole(role)) return { error: "A valid role is required." };
|
|
112
|
+
const cleanUsername = String(username).trim();
|
|
113
|
+
const temporaryPassword = generateTemporaryPassword();
|
|
114
|
+
try {
|
|
115
|
+
const user = await this.client.eponymeUser.create({
|
|
116
|
+
data: {
|
|
117
|
+
id: randomUUID(),
|
|
118
|
+
username: cleanUsername,
|
|
119
|
+
usernameNormalized: normalizeUsername(cleanUsername),
|
|
120
|
+
passwordHash: await hashPassword(temporaryPassword),
|
|
121
|
+
role,
|
|
122
|
+
active: true,
|
|
123
|
+
mustChangePassword: true,
|
|
124
|
+
failedLoginAttempts: 0
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return { result: { user: toAuthUser(user), temporaryPassword } };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (isUniqueConstraintViolation(error)) return { error: "This username is already in use." };
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async updateUser(id, changes) {
|
|
134
|
+
const user = await this.client.eponymeUser.findUnique({ where: { id } });
|
|
135
|
+
if (!user) return { error: "User not found.", notFound: true };
|
|
136
|
+
if (changes.role !== void 0 && !isEponymeRole(changes.role))
|
|
137
|
+
return { error: "A valid role is required." };
|
|
138
|
+
if (changes.active !== void 0 && typeof changes.active !== "boolean")
|
|
139
|
+
return { error: "Active must be a boolean." };
|
|
140
|
+
const nextRole = changes.role ?? user.role;
|
|
141
|
+
const nextActive = changes.active ?? user.active;
|
|
142
|
+
const removesActiveOwner = user.role === "owner" && user.active && (nextRole !== "owner" || !nextActive);
|
|
143
|
+
const apply = async (tx) => {
|
|
144
|
+
if (removesActiveOwner) {
|
|
145
|
+
const activeOwnerCount = await tx.eponymeUser.count({ where: { role: "owner", active: true } });
|
|
146
|
+
if (activeOwnerCount <= 1) return { error: "The last active owner cannot be disabled or demoted." };
|
|
147
|
+
}
|
|
148
|
+
const updated = await tx.eponymeUser.update({
|
|
149
|
+
where: { id },
|
|
150
|
+
data: { role: nextRole, active: nextActive }
|
|
151
|
+
});
|
|
152
|
+
return { user: toAuthUser(updated) };
|
|
153
|
+
};
|
|
154
|
+
const result = removesActiveOwner && this.client.$transaction ? await this.client.$transaction(apply, { isolationLevel: "Serializable" }).catch((error) => isSerializationFailure(error) ? { error: "Another change was applied at the same time. Please try again." } : Promise.reject(error)) : await apply(this.client);
|
|
155
|
+
if (result.error) return result;
|
|
156
|
+
if (!nextActive) await this.client.eponymeUserSession.deleteMany({ where: { userId: id } });
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
async resetPassword(id) {
|
|
160
|
+
const user = await this.client.eponymeUser.findUnique({ where: { id } });
|
|
161
|
+
if (!user) return { error: "User not found." };
|
|
162
|
+
const temporaryPassword = generateTemporaryPassword();
|
|
163
|
+
const updated = await this.client.eponymeUser.update({
|
|
164
|
+
where: { id },
|
|
165
|
+
data: {
|
|
166
|
+
passwordHash: await hashPassword(temporaryPassword),
|
|
167
|
+
mustChangePassword: true,
|
|
168
|
+
failedLoginAttempts: 0,
|
|
169
|
+
lockedUntil: null
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
await this.client.eponymeUserSession.deleteMany({ where: { userId: id } });
|
|
173
|
+
return { result: { user: toAuthUser(updated), temporaryPassword } };
|
|
174
|
+
}
|
|
175
|
+
async createSession(user) {
|
|
176
|
+
const token = randomBytes(32).toString("base64url");
|
|
177
|
+
const expiresAt = new Date(Date.now() + this.sessionDurationDays * 24 * 60 * 60 * 1e3);
|
|
178
|
+
await this.client.eponymeUserSession.create({
|
|
179
|
+
data: {
|
|
180
|
+
id: randomUUID(),
|
|
181
|
+
tokenHash: hashToken(token),
|
|
182
|
+
userId: user.id,
|
|
183
|
+
expiresAt
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return { token, expiresAt, user: toAuthUser(user) };
|
|
187
|
+
}
|
|
188
|
+
async recordFailedLogin(user) {
|
|
189
|
+
if (user.lockedUntil && new Date(user.lockedUntil).getTime() > Date.now()) return;
|
|
190
|
+
const attempts = user.failedLoginAttempts + 1;
|
|
191
|
+
await this.client.eponymeUser.update({
|
|
192
|
+
where: { id: user.id },
|
|
193
|
+
data: {
|
|
194
|
+
failedLoginAttempts: attempts >= MAX_LOGIN_ATTEMPTS ? 0 : attempts,
|
|
195
|
+
lockedUntil: attempts >= MAX_LOGIN_ATTEMPTS ? new Date(Date.now() + LOGIN_LOCK_MS) : null
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function normalizeUsername(username) {
|
|
201
|
+
return username.trim().toLocaleLowerCase("en-US");
|
|
202
|
+
}
|
|
203
|
+
export function validateUsername(username) {
|
|
204
|
+
if (typeof username !== "string" || !username.trim()) return "Username is required.";
|
|
205
|
+
if (!USERNAME_PATTERN.test(username.trim()))
|
|
206
|
+
return "Username must contain 3 to 50 letters, numbers, dots, underscores or hyphens.";
|
|
207
|
+
}
|
|
208
|
+
function hashToken(token) {
|
|
209
|
+
return createHash("sha256").update(token).digest("hex");
|
|
210
|
+
}
|
|
211
|
+
function isUniqueConstraintViolation(error) {
|
|
212
|
+
return Boolean(error && typeof error === "object" && error.code === "P2002");
|
|
213
|
+
}
|
|
214
|
+
function isSerializationFailure(error) {
|
|
215
|
+
return Boolean(error && typeof error === "object" && error.code === "P2034");
|
|
216
|
+
}
|
|
217
|
+
function toAuthUser(user) {
|
|
218
|
+
return {
|
|
219
|
+
id: user.id,
|
|
220
|
+
username: user.username,
|
|
221
|
+
role: isEponymeRole(user.role) ? user.role : "viewer",
|
|
222
|
+
active: user.active,
|
|
223
|
+
mustChangePassword: user.mustChangePassword,
|
|
224
|
+
createdAt: new Date(user.createdAt).toISOString(),
|
|
225
|
+
updatedAt: new Date(user.updatedAt).toISOString()
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import eponymeConfig from "#eponyme/config";
|
|
2
|
+
import prisma from "#eponyme/prisma";
|
|
3
|
+
import { EponymeFormService } from "./eponyme-form-store.js";
|
|
4
|
+
export { EponymeFormService } from "./eponyme-form-store.js";
|
|
5
|
+
const eponymeFormService = new EponymeFormService(eponymeConfig, prisma);
|
|
6
|
+
export function useEponymeFormService() {
|
|
7
|
+
return eponymeFormService;
|
|
8
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { EponymeConfig, EponymeFormDefinitionBase } from '../../types/index.js';
|
|
2
|
+
import { type ValidationErrors } from '../../utils/validate-eponyme-data.js';
|
|
3
|
+
type DateValue = Date | string;
|
|
4
|
+
export interface EponymeFormSubmission {
|
|
5
|
+
id: string;
|
|
6
|
+
formName: string;
|
|
7
|
+
data: Record<string, unknown>;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
export interface EponymeFormSubmissionPage {
|
|
11
|
+
submissions: EponymeFormSubmission[];
|
|
12
|
+
total: number;
|
|
13
|
+
page: number;
|
|
14
|
+
perPage: number;
|
|
15
|
+
}
|
|
16
|
+
export interface PrismaEponymeFormSubmissionRow {
|
|
17
|
+
id: string;
|
|
18
|
+
formName: string;
|
|
19
|
+
data: unknown;
|
|
20
|
+
createdAt: DateValue;
|
|
21
|
+
}
|
|
22
|
+
export type PrismaEponymeFormClient = {
|
|
23
|
+
eponymeFormSubmission: {
|
|
24
|
+
create(args: {
|
|
25
|
+
data: {
|
|
26
|
+
id: string;
|
|
27
|
+
formName: string;
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
}): Promise<PrismaEponymeFormSubmissionRow>;
|
|
31
|
+
findMany(args: {
|
|
32
|
+
where: {
|
|
33
|
+
formName: string;
|
|
34
|
+
};
|
|
35
|
+
orderBy: {
|
|
36
|
+
createdAt: 'desc';
|
|
37
|
+
};
|
|
38
|
+
skip: number;
|
|
39
|
+
take: number;
|
|
40
|
+
}): Promise<PrismaEponymeFormSubmissionRow[]>;
|
|
41
|
+
count(args: {
|
|
42
|
+
where: {
|
|
43
|
+
formName: string;
|
|
44
|
+
};
|
|
45
|
+
}): Promise<number>;
|
|
46
|
+
findUnique(args: {
|
|
47
|
+
where: {
|
|
48
|
+
id: string;
|
|
49
|
+
};
|
|
50
|
+
}): Promise<PrismaEponymeFormSubmissionRow | null>;
|
|
51
|
+
delete(args: {
|
|
52
|
+
where: {
|
|
53
|
+
id: string;
|
|
54
|
+
};
|
|
55
|
+
}): Promise<PrismaEponymeFormSubmissionRow>;
|
|
56
|
+
deleteMany(args: {
|
|
57
|
+
where: {
|
|
58
|
+
formName: string;
|
|
59
|
+
};
|
|
60
|
+
}): Promise<{
|
|
61
|
+
count: number;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export declare class EponymeFormService {
|
|
66
|
+
private readonly client;
|
|
67
|
+
private readonly forms;
|
|
68
|
+
constructor(config: EponymeConfig, client: PrismaEponymeFormClient);
|
|
69
|
+
getForm(name: string): EponymeFormDefinitionBase | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Without this, a missing model surfaces as "Cannot read properties of
|
|
72
|
+
* undefined (reading 'create')", which tells the developer nothing about the
|
|
73
|
+
* two steps they actually skipped.
|
|
74
|
+
*/
|
|
75
|
+
private submissions;
|
|
76
|
+
names(): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Runs the same rules as the dashboard, in `publish` mode: a public form has no
|
|
79
|
+
* draft state, so `required` and the minimum bounds always apply.
|
|
80
|
+
*/
|
|
81
|
+
validate(name: string, payload: unknown): {
|
|
82
|
+
data: Record<string, unknown>;
|
|
83
|
+
} | {
|
|
84
|
+
errors: ValidationErrors;
|
|
85
|
+
} | undefined;
|
|
86
|
+
isHoneypotTriggered(name: string, payload: unknown): boolean;
|
|
87
|
+
submit(name: string, payload: unknown): Promise<{
|
|
88
|
+
submission: EponymeFormSubmission;
|
|
89
|
+
} | {
|
|
90
|
+
errors: ValidationErrors;
|
|
91
|
+
} | undefined>;
|
|
92
|
+
listSubmissions(name: string, options?: {
|
|
93
|
+
page?: number;
|
|
94
|
+
perPage?: number;
|
|
95
|
+
}): Promise<EponymeFormSubmissionPage | undefined>;
|
|
96
|
+
getSubmission(name: string, id: string): Promise<EponymeFormSubmission | undefined>;
|
|
97
|
+
deleteSubmission(name: string, id: string): Promise<boolean>;
|
|
98
|
+
deleteAllSubmissions(name: string): Promise<number | undefined>;
|
|
99
|
+
}
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getEponymeForms } from "../../utils/get-eponyme-schemas.js";
|
|
3
|
+
import { validateEponymeData } from "../../utils/validate-eponyme-data.js";
|
|
4
|
+
const DEFAULT_PER_PAGE = 25;
|
|
5
|
+
const MAX_PER_PAGE = 100;
|
|
6
|
+
export class EponymeFormService {
|
|
7
|
+
constructor(config, client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
this.forms = getEponymeForms(config);
|
|
10
|
+
}
|
|
11
|
+
forms;
|
|
12
|
+
getForm(name) {
|
|
13
|
+
return this.forms[name];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Without this, a missing model surfaces as "Cannot read properties of
|
|
17
|
+
* undefined (reading 'create')", which tells the developer nothing about the
|
|
18
|
+
* two steps they actually skipped.
|
|
19
|
+
*/
|
|
20
|
+
submissions() {
|
|
21
|
+
const delegate = this.client?.eponymeFormSubmission;
|
|
22
|
+
if (!delegate)
|
|
23
|
+
throw new Error("[Eponyme] The EponymeFormSubmission model is missing from your Prisma client. Add it to your schema, run a migration, then run `prisma generate`.");
|
|
24
|
+
return delegate;
|
|
25
|
+
}
|
|
26
|
+
names() {
|
|
27
|
+
return Object.keys(this.forms);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs the same rules as the dashboard, in `publish` mode: a public form has no
|
|
31
|
+
* draft state, so `required` and the minimum bounds always apply.
|
|
32
|
+
*/
|
|
33
|
+
validate(name, payload) {
|
|
34
|
+
const definition = this.forms[name];
|
|
35
|
+
if (!definition) return void 0;
|
|
36
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
37
|
+
return { errors: { _form: ["Body must be an object."] } };
|
|
38
|
+
const input = payload;
|
|
39
|
+
const { [definition.honeypot || ""]: _honeypot, ...rest } = input;
|
|
40
|
+
const unknownKeys = Object.keys(rest).filter((key) => !(key in definition.fields));
|
|
41
|
+
if (unknownKeys.length)
|
|
42
|
+
return { errors: Object.fromEntries(unknownKeys.map((key) => [key, ["Unknown field."]])) };
|
|
43
|
+
const errors = validateEponymeData(definition.fields, rest, "publish");
|
|
44
|
+
if (Object.keys(errors).length) return { errors };
|
|
45
|
+
return { data: rest };
|
|
46
|
+
}
|
|
47
|
+
isHoneypotTriggered(name, payload) {
|
|
48
|
+
const definition = this.forms[name];
|
|
49
|
+
if (!definition || !definition.honeypot) return false;
|
|
50
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
51
|
+
const value = payload[definition.honeypot];
|
|
52
|
+
return typeof value === "string" ? value.trim().length > 0 : value != null && value !== false;
|
|
53
|
+
}
|
|
54
|
+
async submit(name, payload) {
|
|
55
|
+
const definition = this.forms[name];
|
|
56
|
+
if (!definition || definition.submission.mode !== "managed") return void 0;
|
|
57
|
+
const validated = this.validate(name, payload);
|
|
58
|
+
if (!validated || "errors" in validated) return validated;
|
|
59
|
+
const row = await this.submissions().create({
|
|
60
|
+
data: { id: randomUUID(), formName: name, data: validated.data }
|
|
61
|
+
});
|
|
62
|
+
return { submission: toSubmission(row) };
|
|
63
|
+
}
|
|
64
|
+
async listSubmissions(name, options = {}) {
|
|
65
|
+
const definition = this.forms[name];
|
|
66
|
+
if (!definition || definition.submission.mode !== "managed") return void 0;
|
|
67
|
+
const perPage = clamp(options.perPage ?? DEFAULT_PER_PAGE, 1, MAX_PER_PAGE);
|
|
68
|
+
const page = Math.max(1, Math.trunc(options.page ?? 1) || 1);
|
|
69
|
+
const [rows, total] = await Promise.all([
|
|
70
|
+
this.submissions().findMany({
|
|
71
|
+
where: { formName: name },
|
|
72
|
+
orderBy: { createdAt: "desc" },
|
|
73
|
+
skip: (page - 1) * perPage,
|
|
74
|
+
take: perPage
|
|
75
|
+
}),
|
|
76
|
+
this.submissions().count({ where: { formName: name } })
|
|
77
|
+
]);
|
|
78
|
+
return { submissions: rows.map(toSubmission), total, page, perPage };
|
|
79
|
+
}
|
|
80
|
+
async getSubmission(name, id) {
|
|
81
|
+
if (!this.forms[name]) return void 0;
|
|
82
|
+
const row = await this.submissions().findUnique({ where: { id } });
|
|
83
|
+
if (!row || row.formName !== name) return void 0;
|
|
84
|
+
return toSubmission(row);
|
|
85
|
+
}
|
|
86
|
+
async deleteSubmission(name, id) {
|
|
87
|
+
if (!await this.getSubmission(name, id)) return false;
|
|
88
|
+
await this.submissions().delete({ where: { id } });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
async deleteAllSubmissions(name) {
|
|
92
|
+
const definition = this.forms[name];
|
|
93
|
+
if (!definition || definition.submission.mode !== "managed") return void 0;
|
|
94
|
+
const { count } = await this.submissions().deleteMany({ where: { formName: name } });
|
|
95
|
+
return count;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function clamp(value, min, max) {
|
|
99
|
+
if (!Number.isFinite(value)) return min;
|
|
100
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
101
|
+
}
|
|
102
|
+
function toSubmission(row) {
|
|
103
|
+
return {
|
|
104
|
+
id: row.id,
|
|
105
|
+
formName: row.formName,
|
|
106
|
+
data: row.data && typeof row.data === "object" && !Array.isArray(row.data) ? row.data : {},
|
|
107
|
+
createdAt: new Date(row.createdAt).toISOString()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import eponymeConfig from "#eponyme/config";
|
|
2
|
+
import prisma from "#eponyme/prisma";
|
|
3
|
+
import { EponymeService } from "./eponyme-store.js";
|
|
4
|
+
export { EponymeService } from "./eponyme-store.js";
|
|
5
|
+
const eponymeService = new EponymeService(eponymeConfig, prisma);
|
|
6
|
+
export function useEponymeService() {
|
|
7
|
+
return eponymeService;
|
|
8
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { EponymeCollectionDefinitionBase, EponymeConfig, EponymeSchema } from '../../types/index.js';
|
|
2
|
+
import { type ValidationErrors } from '../../utils/validate-eponyme-data.js';
|
|
3
|
+
export type EponymeAction = 'draft' | 'publish';
|
|
4
|
+
export type EponymeVersion = 'draft' | 'published';
|
|
5
|
+
/** Either the live draft/published content, or a numeric id from the version history. */
|
|
6
|
+
export type EponymeVersionSelector = EponymeVersion | number;
|
|
7
|
+
export type EponymeStatus = 'draft' | 'published';
|
|
8
|
+
export interface EponymeResult {
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
status: EponymeStatus;
|
|
11
|
+
publishedAt: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface EponymeVersionAuthor {
|
|
14
|
+
id: string;
|
|
15
|
+
username: string;
|
|
16
|
+
}
|
|
17
|
+
export interface EponymeHistoryEntry {
|
|
18
|
+
id: number;
|
|
19
|
+
action: EponymeAction | 'restore';
|
|
20
|
+
status: EponymeStatus;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
user: EponymeVersionAuthor | null;
|
|
23
|
+
}
|
|
24
|
+
/** Raised when a concurrent write changed the entry between our read and our write. */
|
|
25
|
+
export interface EponymeConflict {
|
|
26
|
+
conflict: true;
|
|
27
|
+
errors?: never;
|
|
28
|
+
}
|
|
29
|
+
export interface EponymeCollectionEntry<Data extends Record<string, unknown> = Record<string, unknown>> {
|
|
30
|
+
slug: string;
|
|
31
|
+
title: string;
|
|
32
|
+
data: Data;
|
|
33
|
+
status: EponymeStatus;
|
|
34
|
+
publishedAt: string | null;
|
|
35
|
+
updatedAt: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface EponymeSitemapEntry {
|
|
38
|
+
loc: string;
|
|
39
|
+
lastmod?: string;
|
|
40
|
+
}
|
|
41
|
+
export type PrismaEponymeRow = {
|
|
42
|
+
name: string;
|
|
43
|
+
data: unknown;
|
|
44
|
+
updatedAt?: Date | string;
|
|
45
|
+
};
|
|
46
|
+
export type PrismaEponymeVersionRow = {
|
|
47
|
+
id: number;
|
|
48
|
+
entryName: string;
|
|
49
|
+
data: unknown;
|
|
50
|
+
action: string;
|
|
51
|
+
status: string;
|
|
52
|
+
createdAt: Date | string;
|
|
53
|
+
userId?: string | null;
|
|
54
|
+
user?: {
|
|
55
|
+
id: string;
|
|
56
|
+
username: string;
|
|
57
|
+
} | null;
|
|
58
|
+
};
|
|
59
|
+
export type PrismaEponymeClient = {
|
|
60
|
+
eponyme: {
|
|
61
|
+
upsert(args: {
|
|
62
|
+
where: {
|
|
63
|
+
name: string;
|
|
64
|
+
};
|
|
65
|
+
create: {
|
|
66
|
+
name: string;
|
|
67
|
+
data: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
update: {
|
|
70
|
+
data?: Record<string, unknown>;
|
|
71
|
+
};
|
|
72
|
+
}): Promise<PrismaEponymeRow>;
|
|
73
|
+
update(args: {
|
|
74
|
+
where: {
|
|
75
|
+
name: string;
|
|
76
|
+
};
|
|
77
|
+
data: {
|
|
78
|
+
data: Record<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
}): Promise<PrismaEponymeRow>;
|
|
81
|
+
/** Used as a compare-and-swap: the `updatedAt` guard makes concurrent writes fail instead of overwriting. */
|
|
82
|
+
updateMany(args: {
|
|
83
|
+
where: {
|
|
84
|
+
name: string;
|
|
85
|
+
updatedAt?: Date | string;
|
|
86
|
+
};
|
|
87
|
+
data: {
|
|
88
|
+
data: Record<string, unknown>;
|
|
89
|
+
};
|
|
90
|
+
}): Promise<{
|
|
91
|
+
count: number;
|
|
92
|
+
}>;
|
|
93
|
+
create(args: {
|
|
94
|
+
data: {
|
|
95
|
+
name: string;
|
|
96
|
+
data: Record<string, unknown>;
|
|
97
|
+
};
|
|
98
|
+
}): Promise<PrismaEponymeRow>;
|
|
99
|
+
findUnique(args: {
|
|
100
|
+
where: {
|
|
101
|
+
name: string;
|
|
102
|
+
};
|
|
103
|
+
}): Promise<PrismaEponymeRow | null>;
|
|
104
|
+
findMany(args: {
|
|
105
|
+
where: {
|
|
106
|
+
name: {
|
|
107
|
+
startsWith: string;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
orderBy: Array<{
|
|
111
|
+
updatedAt: 'desc';
|
|
112
|
+
} | {
|
|
113
|
+
name: 'asc';
|
|
114
|
+
}>;
|
|
115
|
+
}): Promise<PrismaEponymeRow[]>;
|
|
116
|
+
delete(args: {
|
|
117
|
+
where: {
|
|
118
|
+
name: string;
|
|
119
|
+
};
|
|
120
|
+
}): Promise<PrismaEponymeRow>;
|
|
121
|
+
};
|
|
122
|
+
eponymeVersion: {
|
|
123
|
+
create(args: {
|
|
124
|
+
data: {
|
|
125
|
+
entryName: string;
|
|
126
|
+
data: Record<string, unknown>;
|
|
127
|
+
action: string;
|
|
128
|
+
status: EponymeStatus;
|
|
129
|
+
userId?: string | null;
|
|
130
|
+
};
|
|
131
|
+
}): Promise<PrismaEponymeVersionRow>;
|
|
132
|
+
findMany(args: {
|
|
133
|
+
where: {
|
|
134
|
+
entryName: string;
|
|
135
|
+
};
|
|
136
|
+
orderBy: {
|
|
137
|
+
createdAt: 'desc';
|
|
138
|
+
};
|
|
139
|
+
take: number;
|
|
140
|
+
include?: {
|
|
141
|
+
user: true;
|
|
142
|
+
};
|
|
143
|
+
}): Promise<PrismaEponymeVersionRow[]>;
|
|
144
|
+
findUnique(args: {
|
|
145
|
+
where: {
|
|
146
|
+
id: number;
|
|
147
|
+
};
|
|
148
|
+
}): Promise<PrismaEponymeVersionRow | null>;
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
export declare class EponymeService {
|
|
152
|
+
private readonly client;
|
|
153
|
+
private readonly schemas;
|
|
154
|
+
private readonly collections;
|
|
155
|
+
constructor(config: EponymeConfig, client: PrismaEponymeClient);
|
|
156
|
+
getSchema(name: string): EponymeSchema | undefined;
|
|
157
|
+
getCollection(name: string): EponymeCollectionDefinitionBase | undefined;
|
|
158
|
+
private getCollectionEntry;
|
|
159
|
+
/** Reconcile every configured eponyme at application startup. */
|
|
160
|
+
syncAll(): Promise<void>;
|
|
161
|
+
private reconcile;
|
|
162
|
+
private createState;
|
|
163
|
+
private normalizeState;
|
|
164
|
+
/** Reads the stored state along with the `updatedAt` it was read at, used as the optimistic lock token. */
|
|
165
|
+
private loadRow;
|
|
166
|
+
private loadState;
|
|
167
|
+
/**
|
|
168
|
+
* Writes a new state only if the row still carries the `updatedAt` we read.
|
|
169
|
+
* Returns false when a concurrent editor won the race, so the caller can report a conflict
|
|
170
|
+
* instead of silently discarding their changes.
|
|
171
|
+
*/
|
|
172
|
+
private writeState;
|
|
173
|
+
get(name: string, version?: EponymeVersionSelector): Promise<Record<string, unknown> | undefined>;
|
|
174
|
+
getResult(name: string, version?: EponymeVersionSelector): Promise<EponymeResult | undefined>;
|
|
175
|
+
/** Content as it was in a given version, for previewing without restoring it. */
|
|
176
|
+
private getVersionResult;
|
|
177
|
+
statuses(): Promise<Record<string, EponymeStatus>>;
|
|
178
|
+
listCollection(name: string, version?: EponymeVersion): Promise<EponymeCollectionEntry[] | undefined>;
|
|
179
|
+
/**
|
|
180
|
+
* Builds the public URL list configured by `previewPaths`.
|
|
181
|
+
* Collections are resolved directly from their stored rows so this performs
|
|
182
|
+
* one query per collection rather than one query per entry.
|
|
183
|
+
*/
|
|
184
|
+
getSitemapEntries(previewPaths: Record<string, string>): Promise<EponymeSitemapEntry[]>;
|
|
185
|
+
createCollectionEntry(name: string, payload: unknown, actorId?: string): Promise<(EponymeResult & {
|
|
186
|
+
slug: string;
|
|
187
|
+
errors?: never;
|
|
188
|
+
}) | {
|
|
189
|
+
errors: ValidationErrors;
|
|
190
|
+
} | undefined>;
|
|
191
|
+
deleteCollectionEntry(name: string): Promise<boolean>;
|
|
192
|
+
patch(name: string, payload: unknown, action?: EponymeAction, actorId?: string): Promise<(EponymeResult & {
|
|
193
|
+
errors?: never;
|
|
194
|
+
conflict?: never;
|
|
195
|
+
}) | {
|
|
196
|
+
errors: ValidationErrors;
|
|
197
|
+
conflict?: never;
|
|
198
|
+
} | EponymeConflict | undefined>;
|
|
199
|
+
history(name: string, limit?: number): Promise<EponymeHistoryEntry[] | undefined>;
|
|
200
|
+
restore(name: string, versionId: number, actorId?: string): Promise<EponymeResult | EponymeConflict | undefined>;
|
|
201
|
+
}
|
|
202
|
+
export declare function isPrismaError(error: unknown, code: string): boolean;
|