@hywax/cms 2.0.2 → 3.0.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.
Files changed (53) hide show
  1. package/.nuxt/cms/form-panel-section.ts +2 -2
  2. package/.nuxt/cms/{input-uplora-image.ts → form-uplora-image.ts} +2 -9
  3. package/.nuxt/cms/http-statuses.ts +59 -0
  4. package/.nuxt/cms/index.ts +4 -4
  5. package/.nuxt/cms.css +3 -3
  6. package/dist/module.d.mts +5 -2
  7. package/dist/module.json +1 -1
  8. package/dist/module.mjs +77 -46
  9. package/dist/runtime/components/DatePicker.vue +13 -1
  10. package/dist/runtime/components/FormPanel.vue +2 -0
  11. package/dist/runtime/components/{InputSeo.d.vue.ts → FormSeo.d.vue.ts} +10 -8
  12. package/dist/runtime/components/{InputSeo.vue → FormSeo.vue} +18 -8
  13. package/dist/runtime/components/{InputSeo.vue.d.ts → FormSeo.vue.d.ts} +10 -8
  14. package/dist/runtime/components/{InputSlug.vue.d.ts → FormSlug.d.vue.ts} +12 -8
  15. package/dist/runtime/components/{InputSlug.vue → FormSlug.vue} +17 -8
  16. package/dist/runtime/components/{InputSlug.d.vue.ts → FormSlug.vue.d.ts} +12 -8
  17. package/dist/runtime/components/FormUploraImage.d.vue.ts +43 -0
  18. package/dist/runtime/components/FormUploraImage.vue +172 -0
  19. package/dist/runtime/components/FormUploraImage.vue.d.ts +43 -0
  20. package/dist/runtime/components/TablePreviewLink.d.vue.ts +13 -0
  21. package/dist/runtime/components/TablePreviewLink.vue +38 -0
  22. package/dist/runtime/components/TablePreviewLink.vue.d.ts +13 -0
  23. package/dist/runtime/composables/useTableColumns.d.ts +3 -2
  24. package/dist/runtime/composables/useTableColumns.js +11 -12
  25. package/dist/runtime/editor/uplora-image/EditorUploraImageNode.vue +2 -2
  26. package/dist/runtime/server/api/uplora/[id].delete.js +1 -1
  27. package/dist/runtime/server/api/uplora/index.post.js +2 -4
  28. package/dist/runtime/server/errors/HttpError.d.ts +8 -0
  29. package/dist/runtime/server/errors/HttpError.js +7 -0
  30. package/dist/runtime/server/errors/index.d.ts +1 -1
  31. package/dist/runtime/server/errors/index.js +1 -1
  32. package/dist/runtime/server/utils/http.d.ts +43 -0
  33. package/dist/runtime/server/utils/http.js +86 -0
  34. package/dist/runtime/server/utils/validation.js +6 -7
  35. package/dist/runtime/types/index.d.ts +3 -3
  36. package/dist/runtime/types/index.js +3 -3
  37. package/dist/runtime/utils/enums.d.ts +4 -0
  38. package/dist/runtime/utils/enums.js +6 -0
  39. package/dist/runtime/utils/index.d.ts +1 -0
  40. package/dist/runtime/utils/index.js +1 -0
  41. package/package.json +7 -6
  42. package/.nuxt/cms/http-codes.ts +0 -8
  43. package/dist/runtime/components/InputUploraImage.d.vue.ts +0 -40
  44. package/dist/runtime/components/InputUploraImage.vue +0 -181
  45. package/dist/runtime/components/InputUploraImage.vue.d.ts +0 -40
  46. package/dist/runtime/server/errors/InternalHttpError.d.ts +0 -8
  47. package/dist/runtime/server/errors/InternalHttpError.js +0 -7
  48. package/dist/runtime/server/utils/errors.d.ts +0 -8
  49. package/dist/runtime/server/utils/errors.js +0 -57
  50. package/dist/runtime/server/utils/httpHandler.d.ts +0 -10
  51. package/dist/runtime/server/utils/httpHandler.js +0 -15
  52. /package/.nuxt/cms/{input-seo.ts → form-seo.ts} +0 -0
  53. /package/.nuxt/cms/{input-slug.ts → form-slug.ts} +0 -0
@@ -1,30 +1,34 @@
1
1
  import type { AppConfig } from '@nuxt/schema';
2
2
  import type { InputProps } from '@nuxt/ui';
3
3
  import type { ComponentConfig } from '../types';
4
- import theme from '#build/cms/input-slug';
5
- type InputSlug = ComponentConfig<typeof theme, AppConfig, 'inputSlug'>;
6
- export interface InputSlugProps {
4
+ import theme from '#build/cms/form-slug';
5
+ type FormSlug = ComponentConfig<typeof theme, AppConfig, 'formSlug'>;
6
+ export interface FormSlugProps {
7
+ name?: string;
7
8
  regenerate?: boolean;
8
9
  label?: string;
9
10
  titleKey?: string;
10
11
  slugKey?: string;
11
12
  inputProps?: Omit<InputProps, 'modelModifiers'>;
12
- as?: any;
13
13
  class?: any;
14
- ui?: InputSlug['slots'];
14
+ ui?: FormSlug['slots'];
15
15
  }
16
16
  declare const _default: typeof __VLS_export;
17
17
  export default _default;
18
- declare const __VLS_export: import("vue").DefineComponent<InputSlugProps & {
18
+ declare const __VLS_export: import("vue").DefineComponent<FormSlugProps & {
19
19
  title?: string;
20
20
  slug?: string;
21
21
  }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
22
22
  "update:title": (value: string | undefined) => any;
23
23
  "update:slug": (value: string | undefined) => any;
24
- }, string, import("vue").PublicProps, Readonly<InputSlugProps & {
24
+ }, string, import("vue").PublicProps, Readonly<FormSlugProps & {
25
25
  title?: string;
26
26
  slug?: string;
27
27
  }> & Readonly<{
28
28
  "onUpdate:title"?: ((value: string | undefined) => any) | undefined;
29
29
  "onUpdate:slug"?: ((value: string | undefined) => any) | undefined;
30
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ }>, {
31
+ name: string;
32
+ titleKey: string;
33
+ slugKey: string;
34
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,43 @@
1
+ import type { AppConfig } from '@nuxt/schema';
2
+ import type { ComponentConfig } from '../types';
3
+ import theme from '#build/cms/form-uplora-image';
4
+ type FormUploraImage = ComponentConfig<typeof theme, AppConfig, 'formUploraImage'>;
5
+ export interface FormUploraImageModelValue {
6
+ image?: string;
7
+ alt?: string;
8
+ color?: string;
9
+ lqip?: string;
10
+ }
11
+ export interface FormUploraImageProps {
12
+ showExtensions?: boolean;
13
+ id?: string;
14
+ name?: string;
15
+ label?: string;
16
+ disabled?: boolean;
17
+ as?: any;
18
+ class?: any;
19
+ nested?: boolean;
20
+ ui?: FormUploraImage['slots'];
21
+ }
22
+ export interface FormUploraImageEmits {
23
+ upload: [FormUploraImageModelValue];
24
+ delete: [];
25
+ }
26
+ declare const _default: typeof __VLS_export;
27
+ export default _default;
28
+ declare const __VLS_export: import("vue").DefineComponent<FormUploraImageProps & {
29
+ modelValue?: FormUploraImageModelValue;
30
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
31
+ delete: () => any;
32
+ "update:modelValue": (value: FormUploraImageModelValue) => any;
33
+ upload: (args_0: FormUploraImageModelValue) => any;
34
+ }, string, import("vue").PublicProps, Readonly<FormUploraImageProps & {
35
+ modelValue?: FormUploraImageModelValue;
36
+ }> & Readonly<{
37
+ onDelete?: (() => any) | undefined;
38
+ "onUpdate:modelValue"?: ((value: FormUploraImageModelValue) => any) | undefined;
39
+ onUpload?: ((args_0: FormUploraImageModelValue) => any) | undefined;
40
+ }>, {
41
+ showExtensions: boolean;
42
+ nested: boolean;
43
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <UForm
3
+ :name="name"
4
+ :schema="schema"
5
+ :class="ui.root({ class: [props.ui?.root, props.class] })"
6
+ :validate-on="['change']"
7
+ :nested="nested"
8
+ >
9
+ <UFormField name="image" :label="label">
10
+ <div :class="ui.base({ class: [props.ui?.base] })">
11
+ <template v-if="modelValue.image">
12
+ <UPopover :content="{ align: 'center', side: 'top', sideOffset: 8 }" :ui="{ content: 'p-1' }">
13
+ <CUploraImage
14
+ :image="modelValue.image"
15
+ :alt="modelValue.alt"
16
+ :color="modelValue.color"
17
+ :lqip="modelValue.lqip"
18
+ />
19
+
20
+ <template #content>
21
+ <div class="flex items-center gap-0.5">
22
+ <UButton
23
+ color="neutral"
24
+ size="sm"
25
+ variant="ghost"
26
+ :icon="appConfig.ui.icons.refresh"
27
+ @click="resetState()"
28
+ />
29
+
30
+ <ButtonDeleteConfirm
31
+ color="error"
32
+ size="sm"
33
+ variant="ghost"
34
+ label=""
35
+ :icon="appConfig.ui.icons.trash"
36
+ @confirm="deleteExecute(modelValue.image)"
37
+ />
38
+ </div>
39
+ </template>
40
+ </UPopover>
41
+ </template>
42
+ <div v-else :id="id" :class="ui.uploader({ class: [props.ui?.uploader] })">
43
+ <template v-if="uploadStatus === 'pending'">
44
+ <UIcon :name="appConfig.ui.icons.loading" :class="ui.uploaderPendingIcon({ class: [props.ui?.uploaderPendingIcon] })" />
45
+ </template>
46
+ <template v-else-if="uploadStatus === 'idle'">
47
+ <button
48
+ type="button"
49
+ :class="ui.uploaderIdleButton({ class: [props.ui?.uploaderIdleButton] })"
50
+ :disabled="disabled"
51
+ v-bind="ariaAttrs"
52
+ @click="open"
53
+ >
54
+ <UAvatar :icon="appConfig.ui.icons.image" size="lg" />
55
+
56
+ <p :class="ui.uploaderIdleText({ class: [props.ui?.uploaderIdleText] })">
57
+ Загрузить изображение
58
+ </p>
59
+ <p :class="ui.uploaderIdleExtensions({ class: [props.ui?.uploaderIdleExtensions] })">
60
+ {{ extensions }}
61
+ </p>
62
+ </button>
63
+ </template>
64
+ <template v-else-if="uploadStatus === 'error'">
65
+ <p :class="ui.uploaderErrorText({ class: [props.ui?.uploaderErrorText] })">
66
+ Произошла ошибка при загрузке изображения
67
+ </p>
68
+ <div :class="ui.uploaderErrorActions({ class: [props.ui?.uploaderErrorActions] })">
69
+ <UButton
70
+ label="Отменить"
71
+ variant="ghost"
72
+ color="neutral"
73
+ @click="resetUpload"
74
+ />
75
+ <UButton
76
+ label="Повторить"
77
+ variant="soft"
78
+ color="neutral"
79
+ @click="uploadExecute"
80
+ />
81
+ </div>
82
+ </template>
83
+ </div>
84
+ </div>
85
+ </UFormField>
86
+ <UFormField name="alt">
87
+ <UInput
88
+ v-model="modelValue.alt"
89
+ placeholder="Введите описание..."
90
+ variant="none"
91
+ size="xs"
92
+ :ui="{ base: 'text-center' }"
93
+ />
94
+ </UFormField>
95
+ </UForm>
96
+ </template>
97
+
98
+ <script>
99
+ import theme from "#build/cms/form-uplora-image";
100
+ import { useAppConfig, useFormField, useUploraDelete, useUploraUpload } from "#imports";
101
+ import { imagesExtensions } from "@uplora/formats";
102
+ import { computed, useId } from "vue";
103
+ import { z } from "zod";
104
+ import { tv } from "../tv";
105
+ import ButtonDeleteConfirm from "./ButtonDeleteConfirm.vue";
106
+ </script>
107
+
108
+ <script setup>
109
+ const props = defineProps({
110
+ showExtensions: { type: Boolean, required: false, default: true },
111
+ id: { type: String, required: false },
112
+ name: { type: String, required: false },
113
+ label: { type: String, required: false },
114
+ disabled: { type: Boolean, required: false },
115
+ as: { type: null, required: false },
116
+ class: { type: null, required: false },
117
+ nested: { type: Boolean, required: false, default: true },
118
+ ui: { type: null, required: false }
119
+ });
120
+ const emit = defineEmits(["upload", "delete"]);
121
+ const modelValue = defineModel({ type: Object, ...{
122
+ default: () => ({
123
+ image: void 0,
124
+ alt: void 0,
125
+ lqip: void 0,
126
+ color: void 0
127
+ })
128
+ } });
129
+ const appConfig = useAppConfig();
130
+ const schema = z.object({
131
+ image: z.string().min(1),
132
+ alt: z.string().min(1),
133
+ lqip: z.string().min(1).optional(),
134
+ color: z.string().min(1).optional()
135
+ });
136
+ const extensions = computed(() => imagesExtensions.filter((extension) => extension !== "jpeg").join(", "));
137
+ const { id: _id, disabled, emitFormChange, emitFormInput, ariaAttrs } = useFormField(props);
138
+ const id = _id.value ?? useId();
139
+ const { open, execute: uploadExecute, status: uploadStatus, reset: resetUpload, onUploaded } = useUploraUpload({
140
+ accept: "image/*"
141
+ });
142
+ const { execute: deleteExecute, onDeleted } = useUploraDelete();
143
+ function resetState() {
144
+ modelValue.value = {
145
+ image: void 0,
146
+ alt: void 0,
147
+ lqip: void 0,
148
+ color: void 0
149
+ };
150
+ emitFormChange();
151
+ emitFormInput();
152
+ }
153
+ onUploaded((file) => {
154
+ modelValue.value = {
155
+ ...modelValue.value,
156
+ image: file.id,
157
+ lqip: file.lqip,
158
+ color: file.color
159
+ };
160
+ emit("upload", modelValue.value);
161
+ emitFormChange();
162
+ emitFormInput();
163
+ });
164
+ onDeleted(() => {
165
+ resetUpload();
166
+ resetState();
167
+ emit("delete");
168
+ });
169
+ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.cms?.formUploraImage || {} })({
170
+ disabled: disabled.value
171
+ }));
172
+ </script>
@@ -0,0 +1,43 @@
1
+ import type { AppConfig } from '@nuxt/schema';
2
+ import type { ComponentConfig } from '../types';
3
+ import theme from '#build/cms/form-uplora-image';
4
+ type FormUploraImage = ComponentConfig<typeof theme, AppConfig, 'formUploraImage'>;
5
+ export interface FormUploraImageModelValue {
6
+ image?: string;
7
+ alt?: string;
8
+ color?: string;
9
+ lqip?: string;
10
+ }
11
+ export interface FormUploraImageProps {
12
+ showExtensions?: boolean;
13
+ id?: string;
14
+ name?: string;
15
+ label?: string;
16
+ disabled?: boolean;
17
+ as?: any;
18
+ class?: any;
19
+ nested?: boolean;
20
+ ui?: FormUploraImage['slots'];
21
+ }
22
+ export interface FormUploraImageEmits {
23
+ upload: [FormUploraImageModelValue];
24
+ delete: [];
25
+ }
26
+ declare const _default: typeof __VLS_export;
27
+ export default _default;
28
+ declare const __VLS_export: import("vue").DefineComponent<FormUploraImageProps & {
29
+ modelValue?: FormUploraImageModelValue;
30
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
31
+ delete: () => any;
32
+ "update:modelValue": (value: FormUploraImageModelValue) => any;
33
+ upload: (args_0: FormUploraImageModelValue) => any;
34
+ }, string, import("vue").PublicProps, Readonly<FormUploraImageProps & {
35
+ modelValue?: FormUploraImageModelValue;
36
+ }> & Readonly<{
37
+ onDelete?: (() => any) | undefined;
38
+ "onUpdate:modelValue"?: ((value: FormUploraImageModelValue) => any) | undefined;
39
+ onUpload?: ((args_0: FormUploraImageModelValue) => any) | undefined;
40
+ }>, {
41
+ showExtensions: boolean;
42
+ nested: boolean;
43
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,13 @@
1
+ import type { ButtonProps } from '@nuxt/ui';
2
+ import type { RouteLocationRaw } from 'vue-router';
3
+ import type { UploraImageProps } from './UploraImage.vue';
4
+ export interface TablePreviewLinkProps {
5
+ to?: RouteLocationRaw;
6
+ target?: ButtonProps['target'];
7
+ label: string;
8
+ description?: string;
9
+ image?: Pick<UploraImageProps, 'image' | 'alt' | 'color' | 'lqip'>;
10
+ }
11
+ declare const __VLS_export: import("vue").DefineComponent<TablePreviewLinkProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<TablePreviewLinkProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <div class="relative flex items-center gap-2">
3
+ <div v-if="image" class="overflow-hidden rounded-lg">
4
+ <UploraImage
5
+ v-bind="image"
6
+ :sizes="[{ width: 32, height: 32, descriptor: '1x' }]"
7
+ :ui="{ picture: 'aspect-square' }"
8
+ />
9
+ </div>
10
+ <div>
11
+ <ULink
12
+ v-if="to"
13
+ :to="to"
14
+ :target="target"
15
+ class="text-primary font-medium"
16
+ >
17
+ {{ label }}
18
+ </ULink>
19
+ <p v-else class="font-medium text-highlighted text-sm">
20
+ {{ label }}
21
+ </p>
22
+ <p v-if="description" class="text-muted text-xs">
23
+ {{ description }}
24
+ </p>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup>
30
+ import UploraImage from "./UploraImage.vue";
31
+ defineProps({
32
+ to: { type: null, required: false },
33
+ target: { type: [String, Object, null], required: false },
34
+ label: { type: String, required: true },
35
+ description: { type: String, required: false },
36
+ image: { type: Object, required: false }
37
+ });
38
+ </script>
@@ -0,0 +1,13 @@
1
+ import type { ButtonProps } from '@nuxt/ui';
2
+ import type { RouteLocationRaw } from 'vue-router';
3
+ import type { UploraImageProps } from './UploraImage.vue';
4
+ export interface TablePreviewLinkProps {
5
+ to?: RouteLocationRaw;
6
+ target?: ButtonProps['target'];
7
+ label: string;
8
+ description?: string;
9
+ image?: Pick<UploraImageProps, 'image' | 'alt' | 'color' | 'lqip'>;
10
+ }
11
+ declare const __VLS_export: import("vue").DefineComponent<TablePreviewLinkProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<TablePreviewLinkProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
@@ -1,6 +1,7 @@
1
1
  import type { ButtonProps, FormSchema, TableColumn, TableData, TableRow } from '@nuxt/ui';
2
2
  import type { RowData } from '@tanstack/table-core';
3
3
  import type { RouteLocationRaw } from 'vue-router';
4
+ import type { TablePreviewLinkProps } from '../components/TablePreviewLink.vue';
4
5
  import type { FiltersField } from '../types';
5
6
  declare module '@tanstack/table-core' {
6
7
  interface ColumnMeta<TData extends RowData, TValue> {
@@ -18,8 +19,8 @@ export type TableColumnCustom<T extends TableData, D = unknown> = TableColumn<T,
18
19
  to?: ((row: TableRow<T>) => RouteLocationRaw | string | undefined) | RouteLocationRaw | string | undefined;
19
20
  target?: ButtonProps['target'];
20
21
  emptyValue?: string;
21
- image?: string;
22
- description?: string;
22
+ image?: ((row: TableRow<T>) => TablePreviewLinkProps['image'] | undefined) | TablePreviewLinkProps['image'] | undefined;
23
+ description?: ((row: TableRow<T>) => string | undefined) | string | undefined;
23
24
  };
24
25
  export declare function useTableColumns<I extends Record<string, any>, T extends readonly TableColumnCustom<I>[] = readonly TableColumnCustom<I>[]>(columns: T): {
25
26
  columns: {
@@ -1,5 +1,5 @@
1
- import { UUser } from "#components";
2
1
  import { h } from "vue";
2
+ import TablePreviewLink from "../components/TablePreviewLink.vue";
3
3
  import { formatDate, formatDateTime, formatNumber } from "../utils/index.js";
4
4
  export function useTableFiltersFields(_schema, fields) {
5
5
  return { filtersFields: fields };
@@ -28,17 +28,16 @@ function transformColumn(column) {
28
28
  return void 0;
29
29
  }
30
30
  if (column.to) {
31
- return h(UUser, {
32
- to: typeof column.to === "function" ? column.to(row) : column.to,
33
- target: column.target,
34
- name: value.toString(),
35
- class: "inline-flex",
36
- ui: {
37
- name: "text-primary",
38
- avatar: "group-hover/user:scale-100"
39
- },
40
- avatar: column.image ? { src: column.image, size: "sm", alt: value.toString() } : void 0,
41
- description: column.description
31
+ const to = typeof column.to === "function" ? column.to(row) : column.to;
32
+ const target = column.target;
33
+ const description = typeof column.description === "function" ? column.description(row) : column.description;
34
+ const image = typeof column.image === "function" ? column.image(row) : column.image;
35
+ return h(TablePreviewLink, {
36
+ to,
37
+ target,
38
+ description,
39
+ label: value.toString(),
40
+ image
42
41
  });
43
42
  }
44
43
  return value;
@@ -1,13 +1,13 @@
1
1
  <template>
2
2
  <NodeViewWrapper>
3
- <InputUploraImage v-model="attrs" />
3
+ <FormUploraImage v-model="attrs" :nested="false" />
4
4
  </NodeViewWrapper>
5
5
  </template>
6
6
 
7
7
  <script setup>
8
8
  import { NodeViewWrapper } from "@tiptap/vue-3";
9
9
  import { computed } from "vue";
10
- import InputUploraImage from "../../components/InputUploraImage.vue";
10
+ import FormUploraImage from "../../components/FormUploraImage.vue";
11
11
  const props = defineProps({
12
12
  decorations: { type: Array, required: true },
13
13
  selected: { type: Boolean, required: true },
@@ -1,7 +1,7 @@
1
1
  import { useRuntimeConfig } from "#imports";
2
2
  import { getValidatedRouterParams } from "h3";
3
3
  import { z } from "zod";
4
- import { defineHttpHandler } from "../../utils/httpHandler.js";
4
+ import { defineHttpHandler } from "../../utils/http.js";
5
5
  export const filesDeleteRouteParamsSchema = z.object({
6
6
  id: z.string().min(24).max(24)
7
7
  });
@@ -1,10 +1,8 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { HTTP_CODE_BAD_REQUEST } from "#cms/http-codes";
3
2
  import { useRuntimeConfig } from "#imports";
4
3
  import { isImageMimeType } from "@uplora/formats";
5
4
  import { z } from "zod";
6
- import { InternalHttpError } from "../../errors/InternalHttpError.js";
7
- import { defineHttpHandler } from "../../utils/httpHandler.js";
5
+ import { createHttpError, defineHttpHandler } from "../../utils/http.js";
8
6
  import { readValidatedMultipartFormData } from "../../utils/validation.js";
9
7
  const filesCreatePayloadSchema = z.object({
10
8
  file: z.object({
@@ -18,7 +16,7 @@ export default defineHttpHandler(async (event) => {
18
16
  const { file } = await readValidatedMultipartFormData(event, filesCreatePayloadSchema.parse);
19
17
  const { uplora } = useRuntimeConfig();
20
18
  if (!file) {
21
- throw new InternalHttpError(HTTP_CODE_BAD_REQUEST);
19
+ throw createHttpError("badRequest");
22
20
  }
23
21
  const formData = new FormData();
24
22
  formData.append("file", new Blob([file.data], { type: file.type }), file.filename);
@@ -0,0 +1,8 @@
1
+ import type { HttpStatusKey } from '#cms/http-statuses';
2
+ import type { H3Error } from 'h3';
3
+ export type HttpErrorOptions = Partial<Pick<H3Error, 'cause' | 'fatal' | 'data'>>;
4
+ export declare class HttpError extends Error {
5
+ readonly httpStatusKey: HttpStatusKey;
6
+ readonly options?: HttpErrorOptions | undefined;
7
+ constructor(httpStatusKey: HttpStatusKey, options?: HttpErrorOptions | undefined);
8
+ }
@@ -0,0 +1,7 @@
1
+ export class HttpError extends Error {
2
+ constructor(httpStatusKey, options) {
3
+ super("Http Error");
4
+ this.httpStatusKey = httpStatusKey;
5
+ this.options = options;
6
+ }
7
+ }
@@ -1,2 +1,2 @@
1
- export * from './InternalHttpError';
1
+ export * from './HttpError';
2
2
  export * from './TimeoutError';
@@ -1,2 +1,2 @@
1
- export * from "./InternalHttpError.js";
1
+ export * from "./HttpError.js";
2
2
  export * from "./TimeoutError.js";
@@ -0,0 +1,43 @@
1
+ import type { HttpStatusKey } from '#cms/http-statuses';
2
+ import type { EventHandler, EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3';
3
+ import type { CachedEventHandlerOptions } from 'nitropack/types';
4
+ import type { HttpErrorOptions } from '../errors';
5
+ import { H3Error } from 'h3';
6
+ import { HttpError } from '../errors';
7
+ type HttpStatusesCodesMap = Record<string, HttpStatusKey>;
8
+ interface DefineHttpHandlerOptions<Response extends EventHandlerResponse> {
9
+ httpCodesMap?: HttpStatusesCodesMap;
10
+ cache?: CachedEventHandlerOptions<Response>;
11
+ access?: 'admin' | 'employee' | 'user';
12
+ }
13
+ /**
14
+ * Разрешает и форматирует ошибку для HTTP ответов сервера.
15
+ * Логирует ошибку и возвращает отформатированную H3Error с кодом статуса и сообщением.
16
+ *
17
+ * @param event - Объект события.
18
+ * @param error - Ошибка для разрешения.
19
+ * @param httpCodesMap - Опциональное пользовательское сопоставление типов ошибок с ключами статусов.
20
+ * @returns Отформатированная H3Error для ответа.
21
+ */
22
+ export declare function errorServerResolver(event: H3Event, error: unknown, httpCodesMap?: HttpStatusesCodesMap): Promise<H3Error<unknown>>;
23
+ /**
24
+ * Создает новый экземпляр HttpError с заданным ключом статуса и опциями.
25
+ *
26
+ * @param errorStatusKey - Ключ, представляющий HTTP статус.
27
+ * @param options - Дополнительные опции ошибки (cause, fatal).
28
+ * @returns Созданный экземпляр HttpError.
29
+ */
30
+ export declare function createHttpError(errorStatusKey: HttpStatusKey, options?: HttpErrorOptions): HttpError;
31
+ /**
32
+ * Определяет HTTP обработчик, который оборачивает предоставленный обработчик и обрабатывает ошибки.
33
+ * Перехватывает и форматирует ошибки для HTTP ответов, включая ошибки валидации и базы данных.
34
+ *
35
+ * @typeParam Request - Тип объекта запроса.
36
+ * @typeParam Response - Тип объекта ответа.
37
+ * @param handler - Основная функция обработчика.
38
+ * @param options - Опциональное пользовательское сопоставление типов ошибок с ключами статусов.
39
+ * @param options.httpCodesMap - Опциональное пользовательское сопоставление типов ошибок с ключами статусов.
40
+ * @returns Обернутый обработчик событий с обработкой ошибок.
41
+ */
42
+ export declare function defineHttpHandler<Request extends EventHandlerRequest, Response extends EventHandlerResponse>(handler: EventHandler<Request, Response | Promise<Response>>, options?: DefineHttpHandlerOptions<Response>): EventHandler<Request, Promise<Response>>;
43
+ export {};
@@ -0,0 +1,86 @@
1
+ import { httpStatuses } from "#cms/http-statuses";
2
+ import defu from "defu";
3
+ import { DrizzleQueryError } from "drizzle-orm/errors";
4
+ import { createError, eventHandler, H3Error } from "h3";
5
+ import { cachedEventHandler } from "nitropack/runtime";
6
+ import { camelCase } from "scule";
7
+ import { z } from "zod";
8
+ import { logger } from "../../utils/logger.js";
9
+ import { HttpError } from "../errors/index.js";
10
+ const defaultHttpCodes = {
11
+ default: "internalServerError",
12
+ zod: "badRequest",
13
+ db: "internalServerError"
14
+ };
15
+ const excludedLoggedStatuses = ["unauthorized"];
16
+ function extractHttpStatusKey(error, httpCodesMap = {}) {
17
+ const codesMap = defu(httpCodesMap, defaultHttpCodes);
18
+ if (error instanceof HttpError) {
19
+ return error.httpStatusKey;
20
+ } else if (error instanceof z.ZodError) {
21
+ return codesMap.zod;
22
+ } else if (error instanceof DrizzleQueryError) {
23
+ const maybeStatus = `db_${error.cause?.code}`;
24
+ return codesMap[maybeStatus] ?? codesMap.db;
25
+ } else if (error instanceof H3Error && error.cause instanceof HttpError) {
26
+ return error.cause.httpStatusKey;
27
+ } else if (error instanceof Error) {
28
+ const maybeStatus = camelCase(error.message);
29
+ return codesMap[maybeStatus] ?? codesMap.default;
30
+ }
31
+ return codesMap.default;
32
+ }
33
+ function enrichHttpError(error) {
34
+ if (error instanceof z.ZodError) {
35
+ return { data: error.issues };
36
+ }
37
+ if (error instanceof HttpError) {
38
+ return { data: error.options?.data };
39
+ }
40
+ return {};
41
+ }
42
+ export async function errorServerResolver(event, error, httpCodesMap = {}) {
43
+ const httpStatusKey = extractHttpStatusKey(error, httpCodesMap);
44
+ const httpStatusCode = httpStatuses[httpStatusKey].code;
45
+ const httpStatusMessage = httpStatuses[httpStatusKey].message;
46
+ if (!excludedLoggedStatuses.includes(httpStatusKey)) {
47
+ logger.error.raw({
48
+ key: httpStatusKey,
49
+ code: httpStatusCode,
50
+ path: event.path,
51
+ method: event.method,
52
+ ...error instanceof HttpError ? { data: JSON.stringify(error.options?.data) } : { error: JSON.stringify(error) }
53
+ });
54
+ }
55
+ return createError(defu(
56
+ {
57
+ statusCode: httpStatusCode,
58
+ message: httpStatusMessage
59
+ },
60
+ enrichHttpError(error)
61
+ ));
62
+ }
63
+ export function createHttpError(errorStatusKey, options) {
64
+ return new HttpError(errorStatusKey, options);
65
+ }
66
+ export function defineHttpHandler(handler, options) {
67
+ async function handlerWrapper(event) {
68
+ try {
69
+ return await handler(event);
70
+ } catch (e) {
71
+ let error = e;
72
+ if (error.cause?.data instanceof Error) {
73
+ error = error.cause?.data;
74
+ }
75
+ throw await errorServerResolver(event, error, options?.httpCodesMap);
76
+ }
77
+ }
78
+ if (options?.cache) {
79
+ return cachedEventHandler(handlerWrapper, {
80
+ group: "http",
81
+ swr: false,
82
+ ...options.cache
83
+ });
84
+ }
85
+ return eventHandler(handlerWrapper);
86
+ }