@btst/stack 1.5.2 → 1.6.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/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.cjs +68 -0
- package/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.mjs +60 -0
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +3937 -0
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +3907 -0
- package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.cjs +30 -0
- package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.mjs +28 -0
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +675 -0
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +661 -0
- package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.cjs +358 -0
- package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.mjs +332 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.cjs +211 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.mjs +188 -0
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +3 -2
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +3 -2
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +15 -15
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +16 -16
- package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.cjs +588 -0
- package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.mjs +586 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +131 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +129 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.cjs +32 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.mjs +30 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.cjs +21 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.mjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.cjs +34 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.mjs +32 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.cjs +20 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.mjs +18 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.cjs +186 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.mjs +184 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.cjs +165 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.mjs +163 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +177 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +175 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.cjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.mjs +15 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.cjs +16 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.mjs +14 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.cjs +27 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.mjs +25 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.cjs +39 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.mjs +37 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.cjs +551 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.mjs +537 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.cjs +36 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.mjs +34 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.cjs +21 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.mjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.cjs +14 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.mjs +12 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.cjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.mjs +15 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.cjs +278 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.mjs +276 -0
- package/dist/packages/better-stack/src/plugins/form-builder/db.cjs +99 -0
- package/dist/packages/better-stack/src/plugins/form-builder/db.mjs +97 -0
- package/dist/packages/better-stack/src/plugins/form-builder/schemas.cjs +82 -0
- package/dist/packages/better-stack/src/plugins/form-builder/schemas.mjs +74 -0
- package/dist/packages/better-stack/src/plugins/form-builder/utils.cjs +37 -0
- package/dist/packages/better-stack/src/plugins/form-builder/utils.mjs +29 -0
- package/dist/packages/ui/src/components/auto-form/index.cjs +2 -12
- package/dist/packages/ui/src/components/auto-form/index.mjs +2 -9
- package/dist/packages/ui/src/components/auto-form/stepped-auto-form.cjs +377 -0
- package/dist/packages/ui/src/components/auto-form/stepped-auto-form.mjs +368 -0
- package/dist/packages/ui/src/components/auto-form/utils.cjs +1 -56
- package/dist/packages/ui/src/components/auto-form/utils.mjs +2 -56
- package/dist/packages/ui/src/components/form-builder/canvas.cjs +111 -0
- package/dist/packages/ui/src/components/form-builder/canvas.mjs +109 -0
- package/dist/packages/ui/src/components/form-builder/components/index.cjs +570 -0
- package/dist/packages/ui/src/components/form-builder/components/index.mjs +553 -0
- package/dist/packages/ui/src/components/form-builder/edit-field-dialog.cjs +131 -0
- package/dist/packages/ui/src/components/form-builder/edit-field-dialog.mjs +129 -0
- package/dist/packages/ui/src/components/form-builder/form-preview.cjs +73 -0
- package/dist/packages/ui/src/components/form-builder/form-preview.mjs +71 -0
- package/dist/packages/ui/src/components/form-builder/index.cjs +353 -0
- package/dist/packages/ui/src/components/form-builder/index.mjs +344 -0
- package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.cjs +263 -0
- package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.mjs +261 -0
- package/dist/packages/ui/src/components/form-builder/palette.cjs +52 -0
- package/dist/packages/ui/src/components/form-builder/palette.mjs +49 -0
- package/dist/packages/ui/src/components/form-builder/schema-utils.cjs +120 -0
- package/dist/packages/ui/src/components/form-builder/schema-utils.mjs +114 -0
- package/dist/packages/ui/src/components/form-builder/sortable-field.cjs +151 -0
- package/dist/packages/ui/src/components/form-builder/sortable-field.mjs +148 -0
- package/dist/packages/ui/src/components/form-builder/step-tabs.cjs +180 -0
- package/dist/packages/ui/src/components/form-builder/step-tabs.mjs +178 -0
- package/dist/packages/ui/src/components/form-builder/types.cjs +7 -0
- package/dist/packages/ui/src/components/form-builder/types.mjs +5 -0
- package/dist/packages/ui/src/components/form-builder/validation-schemas.cjs +67 -0
- package/dist/packages/ui/src/components/form-builder/validation-schemas.mjs +56 -0
- package/dist/packages/ui/src/components/tabs.cjs +70 -0
- package/dist/packages/ui/src/components/tabs.mjs +65 -0
- package/dist/packages/ui/src/lib/schema-converter.cjs +130 -0
- package/dist/packages/ui/src/lib/schema-converter.mjs +124 -0
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/client/index.cjs +6 -0
- package/dist/plugins/cms/client/index.d.cts +6 -113
- package/dist/plugins/cms/client/index.d.mts +6 -113
- package/dist/plugins/cms/client/index.d.ts +6 -113
- package/dist/plugins/cms/client/index.mjs +1 -0
- package/dist/plugins/form-builder/api/index.cjs +7 -0
- package/dist/plugins/form-builder/api/index.d.cts +141 -0
- package/dist/plugins/form-builder/api/index.d.mts +141 -0
- package/dist/plugins/form-builder/api/index.d.ts +141 -0
- package/dist/plugins/form-builder/api/index.mjs +1 -0
- package/dist/plugins/form-builder/client/components/index.cjs +29 -0
- package/dist/plugins/form-builder/client/components/index.d.cts +93 -0
- package/dist/plugins/form-builder/client/components/index.d.mts +93 -0
- package/dist/plugins/form-builder/client/components/index.d.ts +93 -0
- package/dist/plugins/form-builder/client/components/index.mjs +18 -0
- package/dist/plugins/form-builder/client/hooks/index.cjs +19 -0
- package/dist/plugins/form-builder/client/hooks/index.d.cts +154 -0
- package/dist/plugins/form-builder/client/hooks/index.d.mts +154 -0
- package/dist/plugins/form-builder/client/hooks/index.d.ts +154 -0
- package/dist/plugins/form-builder/client/hooks/index.mjs +1 -0
- package/dist/plugins/form-builder/client/index.cjs +13 -0
- package/dist/plugins/form-builder/client/index.d.cts +381 -0
- package/dist/plugins/form-builder/client/index.d.mts +381 -0
- package/dist/plugins/form-builder/client/index.d.ts +381 -0
- package/dist/plugins/form-builder/client/index.mjs +2 -0
- package/dist/plugins/form-builder/client.css +3 -0
- package/dist/plugins/form-builder/query-keys.cjs +143 -0
- package/dist/plugins/form-builder/query-keys.d.cts +74 -0
- package/dist/plugins/form-builder/query-keys.d.mts +74 -0
- package/dist/plugins/form-builder/query-keys.d.ts +74 -0
- package/dist/plugins/form-builder/query-keys.mjs +141 -0
- package/dist/plugins/form-builder/style.css +19 -0
- package/dist/shared/stack.AX5nZ6A3.d.cts +86 -0
- package/dist/shared/stack.AX5nZ6A3.d.mts +86 -0
- package/dist/shared/stack.AX5nZ6A3.d.ts +86 -0
- package/dist/shared/stack.BIh2AXaW.d.cts +123 -0
- package/dist/shared/stack.BIh2AXaW.d.mts +123 -0
- package/dist/shared/stack.BIh2AXaW.d.ts +123 -0
- package/dist/shared/stack.DzH_wcvr.d.cts +195 -0
- package/dist/shared/stack.DzH_wcvr.d.mts +195 -0
- package/dist/shared/stack.DzH_wcvr.d.ts +195 -0
- package/package.json +54 -1
- package/src/plugins/cms/api/plugin.ts +9 -4
- package/src/plugins/cms/client/components/forms/content-form.tsx +23 -25
- package/src/plugins/cms/client/index.ts +11 -0
- package/src/plugins/form-builder/api/index.ts +1 -0
- package/src/plugins/form-builder/api/plugin.ts +776 -0
- package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +253 -0
- package/src/plugins/form-builder/client/components/index.tsx +24 -0
- package/src/plugins/form-builder/client/components/loading/form-builder-skeleton.tsx +42 -0
- package/src/plugins/form-builder/client/components/loading/form-list-skeleton.tsx +25 -0
- package/src/plugins/form-builder/client/components/loading/index.tsx +3 -0
- package/src/plugins/form-builder/client/components/loading/submissions-skeleton.tsx +40 -0
- package/src/plugins/form-builder/client/components/pages/404-page.tsx +28 -0
- package/src/plugins/form-builder/client/components/pages/form-builder-page.internal.tsx +253 -0
- package/src/plugins/form-builder/client/components/pages/form-builder-page.tsx +26 -0
- package/src/plugins/form-builder/client/components/pages/form-list-page.internal.tsx +231 -0
- package/src/plugins/form-builder/client/components/pages/form-list-page.tsx +22 -0
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +268 -0
- package/src/plugins/form-builder/client/components/pages/submissions-page.tsx +26 -0
- package/src/plugins/form-builder/client/components/shared/default-error.tsx +30 -0
- package/src/plugins/form-builder/client/components/shared/empty-state.tsx +26 -0
- package/src/plugins/form-builder/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/form-builder/client/components/shared/pagination.tsx +52 -0
- package/src/plugins/form-builder/client/hooks/form-builder-hooks.tsx +799 -0
- package/src/plugins/form-builder/client/hooks/index.tsx +1 -0
- package/src/plugins/form-builder/client/index.ts +22 -0
- package/src/plugins/form-builder/client/localization/form-builder-common.ts +36 -0
- package/src/plugins/form-builder/client/localization/form-builder-editor.ts +18 -0
- package/src/plugins/form-builder/client/localization/form-builder-list.ts +17 -0
- package/src/plugins/form-builder/client/localization/form-builder-submissions.ts +17 -0
- package/src/plugins/form-builder/client/localization/form-builder-toasts.ts +10 -0
- package/src/plugins/form-builder/client/localization/index.ts +15 -0
- package/src/plugins/form-builder/client/overrides.ts +146 -0
- package/src/plugins/form-builder/client/plugin.tsx +488 -0
- package/src/plugins/form-builder/client.css +3 -0
- package/src/plugins/form-builder/db.ts +99 -0
- package/src/plugins/form-builder/query-keys.ts +198 -0
- package/src/plugins/form-builder/schemas.ts +122 -0
- package/src/plugins/form-builder/style.css +19 -0
- package/src/plugins/form-builder/types.ts +317 -0
- package/src/plugins/form-builder/utils.ts +63 -0
- package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import { defineBackendPlugin } from "@btst/stack/plugins/api";
|
|
3
|
+
import { createEndpoint } from "@btst/stack/plugins/api";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { formSchemaToZod } from "@workspace/ui/lib/schema-converter";
|
|
6
|
+
import { formBuilderSchema as dbSchema } from "../db";
|
|
7
|
+
import type {
|
|
8
|
+
Form,
|
|
9
|
+
FormSubmission,
|
|
10
|
+
FormSubmissionWithForm,
|
|
11
|
+
FormBuilderBackendConfig,
|
|
12
|
+
FormBuilderHookContext,
|
|
13
|
+
SubmissionHookContext,
|
|
14
|
+
SerializedForm,
|
|
15
|
+
SerializedFormSubmission,
|
|
16
|
+
SerializedFormSubmissionWithData,
|
|
17
|
+
FormInput,
|
|
18
|
+
FormUpdate,
|
|
19
|
+
} from "../types";
|
|
20
|
+
import {
|
|
21
|
+
listFormsQuerySchema,
|
|
22
|
+
createFormSchema,
|
|
23
|
+
updateFormSchema,
|
|
24
|
+
listSubmissionsQuerySchema,
|
|
25
|
+
} from "../schemas";
|
|
26
|
+
import { slugify, extractIpAddress, extractUserAgent } from "../utils";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serialize a Form for API response (convert dates to strings)
|
|
30
|
+
*/
|
|
31
|
+
function serializeForm(form: Form): SerializedForm {
|
|
32
|
+
return {
|
|
33
|
+
id: form.id,
|
|
34
|
+
name: form.name,
|
|
35
|
+
slug: form.slug,
|
|
36
|
+
description: form.description,
|
|
37
|
+
schema: form.schema,
|
|
38
|
+
successMessage: form.successMessage,
|
|
39
|
+
redirectUrl: form.redirectUrl,
|
|
40
|
+
status: form.status,
|
|
41
|
+
createdBy: form.createdBy,
|
|
42
|
+
createdAt: form.createdAt.toISOString(),
|
|
43
|
+
updatedAt: form.updatedAt.toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Serialize a FormSubmission for API response (convert dates to strings)
|
|
49
|
+
*/
|
|
50
|
+
function serializeFormSubmission(
|
|
51
|
+
submission: FormSubmission,
|
|
52
|
+
): SerializedFormSubmission {
|
|
53
|
+
return {
|
|
54
|
+
...submission,
|
|
55
|
+
submittedAt: submission.submittedAt.toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Serialize a FormSubmission with parsed data and joined Form
|
|
61
|
+
*/
|
|
62
|
+
function serializeFormSubmissionWithData(
|
|
63
|
+
submission: FormSubmissionWithForm,
|
|
64
|
+
): SerializedFormSubmissionWithData {
|
|
65
|
+
return {
|
|
66
|
+
...serializeFormSubmission(submission),
|
|
67
|
+
parsedData: JSON.parse(submission.data),
|
|
68
|
+
form: submission.form ? serializeForm(submission.form) : undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Form Builder backend plugin
|
|
74
|
+
* Provides API endpoints for managing forms and form submissions
|
|
75
|
+
*
|
|
76
|
+
* @param config - Configuration with optional hooks
|
|
77
|
+
*/
|
|
78
|
+
export const formBuilderBackendPlugin = (
|
|
79
|
+
config: FormBuilderBackendConfig = {},
|
|
80
|
+
) =>
|
|
81
|
+
defineBackendPlugin({
|
|
82
|
+
name: "form-builder",
|
|
83
|
+
|
|
84
|
+
dbPlugin: dbSchema,
|
|
85
|
+
|
|
86
|
+
routes: (adapter: Adapter) => {
|
|
87
|
+
// Helper to create hook context from request
|
|
88
|
+
const createContext = (headers?: Headers): FormBuilderHookContext => ({
|
|
89
|
+
headers,
|
|
90
|
+
ipAddress: extractIpAddress(headers),
|
|
91
|
+
userAgent: extractUserAgent(headers),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Helper to create submission hook context
|
|
95
|
+
const createSubmissionContext = (
|
|
96
|
+
formSlug: string,
|
|
97
|
+
formId: string,
|
|
98
|
+
headers?: Headers,
|
|
99
|
+
): SubmissionHookContext => ({
|
|
100
|
+
...createContext(headers),
|
|
101
|
+
formSlug,
|
|
102
|
+
formId,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ========== Form CRUD Endpoints (Admin) ==========
|
|
106
|
+
|
|
107
|
+
const listForms = createEndpoint(
|
|
108
|
+
"/forms",
|
|
109
|
+
{
|
|
110
|
+
method: "GET",
|
|
111
|
+
query: listFormsQuerySchema,
|
|
112
|
+
},
|
|
113
|
+
async (ctx) => {
|
|
114
|
+
const { status, limit, offset } = ctx.query;
|
|
115
|
+
const context = createContext(ctx.headers);
|
|
116
|
+
|
|
117
|
+
// Call before hook for auth check
|
|
118
|
+
if (config.hooks?.onBeforeListForms) {
|
|
119
|
+
const canList = await config.hooks.onBeforeListForms(context);
|
|
120
|
+
if (!canList) {
|
|
121
|
+
throw ctx.error(403, { message: "Access denied" });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const whereConditions: Array<{
|
|
126
|
+
field: string;
|
|
127
|
+
value: string;
|
|
128
|
+
operator: "eq";
|
|
129
|
+
}> = [];
|
|
130
|
+
if (status) {
|
|
131
|
+
whereConditions.push({
|
|
132
|
+
field: "status",
|
|
133
|
+
value: status,
|
|
134
|
+
operator: "eq" as const,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get total count
|
|
139
|
+
const allForms = await adapter.findMany<Form>({
|
|
140
|
+
model: "form",
|
|
141
|
+
where: whereConditions.length > 0 ? whereConditions : undefined,
|
|
142
|
+
});
|
|
143
|
+
const total = allForms.length;
|
|
144
|
+
|
|
145
|
+
// Get paginated forms
|
|
146
|
+
const forms = await adapter.findMany<Form>({
|
|
147
|
+
model: "form",
|
|
148
|
+
where: whereConditions.length > 0 ? whereConditions : undefined,
|
|
149
|
+
limit,
|
|
150
|
+
offset,
|
|
151
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
items: forms.map(serializeForm),
|
|
156
|
+
total,
|
|
157
|
+
limit,
|
|
158
|
+
offset,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const getFormBySlug = createEndpoint(
|
|
164
|
+
"/forms/:slug",
|
|
165
|
+
{
|
|
166
|
+
method: "GET",
|
|
167
|
+
params: z.object({ slug: z.string() }),
|
|
168
|
+
},
|
|
169
|
+
async (ctx) => {
|
|
170
|
+
const { slug } = ctx.params;
|
|
171
|
+
const context = createContext(ctx.headers);
|
|
172
|
+
|
|
173
|
+
// Call before hook for access check
|
|
174
|
+
if (config.hooks?.onBeforeGetForm) {
|
|
175
|
+
const canGet = await config.hooks.onBeforeGetForm(slug, context);
|
|
176
|
+
if (!canGet) {
|
|
177
|
+
throw ctx.error(403, { message: "Access denied" });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const form = await adapter.findOne<Form>({
|
|
182
|
+
model: "form",
|
|
183
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!form) {
|
|
187
|
+
throw ctx.error(404, { message: "Form not found" });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return serializeForm(form);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const getFormById = createEndpoint(
|
|
195
|
+
"/forms/id/:id",
|
|
196
|
+
{
|
|
197
|
+
method: "GET",
|
|
198
|
+
params: z.object({ id: z.string() }),
|
|
199
|
+
},
|
|
200
|
+
async (ctx) => {
|
|
201
|
+
const { id } = ctx.params;
|
|
202
|
+
const context = createContext(ctx.headers);
|
|
203
|
+
|
|
204
|
+
// Call before hook for access check
|
|
205
|
+
if (config.hooks?.onBeforeGetForm) {
|
|
206
|
+
const canGet = await config.hooks.onBeforeGetForm(id, context);
|
|
207
|
+
if (!canGet) {
|
|
208
|
+
throw ctx.error(403, { message: "Access denied" });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const form = await adapter.findOne<Form>({
|
|
213
|
+
model: "form",
|
|
214
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!form) {
|
|
218
|
+
throw ctx.error(404, { message: "Form not found" });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return serializeForm(form);
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const createForm = createEndpoint(
|
|
226
|
+
"/forms",
|
|
227
|
+
{
|
|
228
|
+
method: "POST",
|
|
229
|
+
body: createFormSchema,
|
|
230
|
+
},
|
|
231
|
+
async (ctx) => {
|
|
232
|
+
const body = ctx.body;
|
|
233
|
+
const context = createContext(ctx.headers);
|
|
234
|
+
|
|
235
|
+
// Sanitize slug to ensure it's URL-safe
|
|
236
|
+
const slug = slugify(body.slug);
|
|
237
|
+
|
|
238
|
+
if (!slug) {
|
|
239
|
+
throw ctx.error(400, {
|
|
240
|
+
message:
|
|
241
|
+
"Invalid slug: must contain at least one alphanumeric character",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for duplicate slug
|
|
246
|
+
const existing = await adapter.findOne<Form>({
|
|
247
|
+
model: "form",
|
|
248
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
249
|
+
});
|
|
250
|
+
if (existing) {
|
|
251
|
+
throw ctx.error(409, {
|
|
252
|
+
message: "Form with this slug already exists",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate JSON Schema
|
|
257
|
+
try {
|
|
258
|
+
JSON.parse(body.schema);
|
|
259
|
+
} catch {
|
|
260
|
+
throw ctx.error(400, { message: "Invalid JSON Schema" });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Build form input
|
|
264
|
+
let formInput: FormInput = {
|
|
265
|
+
name: body.name,
|
|
266
|
+
slug,
|
|
267
|
+
description: body.description,
|
|
268
|
+
schema: body.schema,
|
|
269
|
+
successMessage: body.successMessage,
|
|
270
|
+
redirectUrl: body.redirectUrl || undefined,
|
|
271
|
+
status: body.status as "active" | "inactive" | "archived",
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Call before hook - may modify data or deny operation
|
|
275
|
+
if (config.hooks?.onBeforeFormCreated) {
|
|
276
|
+
const result = await config.hooks.onBeforeFormCreated(
|
|
277
|
+
formInput,
|
|
278
|
+
context,
|
|
279
|
+
);
|
|
280
|
+
if (result === false) {
|
|
281
|
+
throw ctx.error(403, { message: "Create operation denied" });
|
|
282
|
+
}
|
|
283
|
+
if (result && typeof result === "object") {
|
|
284
|
+
formInput = result;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const form = await adapter.create<Form>({
|
|
289
|
+
model: "form",
|
|
290
|
+
data: {
|
|
291
|
+
name: formInput.name,
|
|
292
|
+
slug: formInput.slug,
|
|
293
|
+
description: formInput.description,
|
|
294
|
+
schema: formInput.schema,
|
|
295
|
+
successMessage: formInput.successMessage,
|
|
296
|
+
redirectUrl: formInput.redirectUrl,
|
|
297
|
+
status: formInput.status || "active",
|
|
298
|
+
createdBy: formInput.createdBy,
|
|
299
|
+
createdAt: new Date(),
|
|
300
|
+
updatedAt: new Date(),
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const serialized = serializeForm(form as Form);
|
|
305
|
+
|
|
306
|
+
// Call after hook
|
|
307
|
+
if (config.hooks?.onAfterFormCreated) {
|
|
308
|
+
await config.hooks.onAfterFormCreated(serialized, context);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return serialized;
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const updateForm = createEndpoint(
|
|
316
|
+
"/forms/:id",
|
|
317
|
+
{
|
|
318
|
+
method: "PUT",
|
|
319
|
+
params: z.object({ id: z.string() }),
|
|
320
|
+
body: updateFormSchema,
|
|
321
|
+
},
|
|
322
|
+
async (ctx) => {
|
|
323
|
+
const { id } = ctx.params;
|
|
324
|
+
const body = ctx.body;
|
|
325
|
+
const context = createContext(ctx.headers);
|
|
326
|
+
|
|
327
|
+
const existing = await adapter.findOne<Form>({
|
|
328
|
+
model: "form",
|
|
329
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!existing) {
|
|
333
|
+
throw ctx.error(404, { message: "Form not found" });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Sanitize slug if provided
|
|
337
|
+
let slug: string | undefined;
|
|
338
|
+
if (body.slug) {
|
|
339
|
+
slug = slugify(body.slug);
|
|
340
|
+
if (!slug) {
|
|
341
|
+
throw ctx.error(400, {
|
|
342
|
+
message:
|
|
343
|
+
"Invalid slug: must contain at least one alphanumeric character",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check for duplicate slug if changing
|
|
348
|
+
if (slug !== existing.slug) {
|
|
349
|
+
const duplicate = await adapter.findOne<Form>({
|
|
350
|
+
model: "form",
|
|
351
|
+
where: [
|
|
352
|
+
{ field: "slug", value: slug, operator: "eq" as const },
|
|
353
|
+
],
|
|
354
|
+
});
|
|
355
|
+
if (duplicate) {
|
|
356
|
+
throw ctx.error(409, {
|
|
357
|
+
message: "Form with this slug already exists",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Validate JSON Schema if provided
|
|
364
|
+
if (body.schema) {
|
|
365
|
+
try {
|
|
366
|
+
JSON.parse(body.schema);
|
|
367
|
+
} catch {
|
|
368
|
+
throw ctx.error(400, { message: "Invalid JSON Schema" });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Build update input
|
|
373
|
+
let updateInput: FormUpdate = {
|
|
374
|
+
name: body.name,
|
|
375
|
+
slug,
|
|
376
|
+
description: body.description,
|
|
377
|
+
schema: body.schema,
|
|
378
|
+
successMessage: body.successMessage,
|
|
379
|
+
redirectUrl: body.redirectUrl,
|
|
380
|
+
status: body.status as
|
|
381
|
+
| "active"
|
|
382
|
+
| "inactive"
|
|
383
|
+
| "archived"
|
|
384
|
+
| undefined,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Call before hook - may modify data or deny operation
|
|
388
|
+
if (config.hooks?.onBeforeFormUpdated) {
|
|
389
|
+
const result = await config.hooks.onBeforeFormUpdated(
|
|
390
|
+
id,
|
|
391
|
+
updateInput,
|
|
392
|
+
context,
|
|
393
|
+
);
|
|
394
|
+
if (result === false) {
|
|
395
|
+
throw ctx.error(403, { message: "Update operation denied" });
|
|
396
|
+
}
|
|
397
|
+
if (result && typeof result === "object") {
|
|
398
|
+
updateInput = result;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Build update data
|
|
403
|
+
const updateData: Partial<Form> = {
|
|
404
|
+
updatedAt: new Date(),
|
|
405
|
+
};
|
|
406
|
+
if (updateInput.name) updateData.name = updateInput.name;
|
|
407
|
+
if (updateInput.slug) updateData.slug = updateInput.slug;
|
|
408
|
+
if (updateInput.description !== undefined)
|
|
409
|
+
updateData.description = updateInput.description;
|
|
410
|
+
if (updateInput.schema) updateData.schema = updateInput.schema;
|
|
411
|
+
if (updateInput.successMessage !== undefined)
|
|
412
|
+
updateData.successMessage = updateInput.successMessage;
|
|
413
|
+
if (updateInput.redirectUrl !== undefined)
|
|
414
|
+
updateData.redirectUrl = updateInput.redirectUrl;
|
|
415
|
+
if (updateInput.status) updateData.status = updateInput.status;
|
|
416
|
+
|
|
417
|
+
await adapter.update({
|
|
418
|
+
model: "form",
|
|
419
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
420
|
+
update: updateData,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const updated = await adapter.findOne<Form>({
|
|
424
|
+
model: "form",
|
|
425
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (!updated) {
|
|
429
|
+
throw ctx.error(500, { message: "Failed to fetch updated form" });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const serialized = serializeForm(updated);
|
|
433
|
+
|
|
434
|
+
// Call after hook
|
|
435
|
+
if (config.hooks?.onAfterFormUpdated) {
|
|
436
|
+
await config.hooks.onAfterFormUpdated(serialized, context);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return serialized;
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const deleteForm = createEndpoint(
|
|
444
|
+
"/forms/:id",
|
|
445
|
+
{
|
|
446
|
+
method: "DELETE",
|
|
447
|
+
params: z.object({ id: z.string() }),
|
|
448
|
+
},
|
|
449
|
+
async (ctx) => {
|
|
450
|
+
const { id } = ctx.params;
|
|
451
|
+
const context = createContext(ctx.headers);
|
|
452
|
+
|
|
453
|
+
const existing = await adapter.findOne<Form>({
|
|
454
|
+
model: "form",
|
|
455
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (!existing) {
|
|
459
|
+
throw ctx.error(404, { message: "Form not found" });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Call before hook
|
|
463
|
+
if (config.hooks?.onBeforeFormDeleted) {
|
|
464
|
+
const canDelete = await config.hooks.onBeforeFormDeleted(
|
|
465
|
+
id,
|
|
466
|
+
context,
|
|
467
|
+
);
|
|
468
|
+
if (!canDelete) {
|
|
469
|
+
throw ctx.error(403, { message: "Delete operation denied" });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Delete associated submissions first (cascade)
|
|
474
|
+
await adapter.delete({
|
|
475
|
+
model: "formSubmission",
|
|
476
|
+
where: [{ field: "formId", value: id, operator: "eq" as const }],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
await adapter.delete({
|
|
480
|
+
model: "form",
|
|
481
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Call after hook
|
|
485
|
+
if (config.hooks?.onAfterFormDeleted) {
|
|
486
|
+
await config.hooks.onAfterFormDeleted(id, context);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { success: true };
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// ========== Form Submission Endpoints ==========
|
|
494
|
+
|
|
495
|
+
const submitForm = createEndpoint(
|
|
496
|
+
"/forms/:slug/submit",
|
|
497
|
+
{
|
|
498
|
+
method: "POST",
|
|
499
|
+
params: z.object({ slug: z.string() }),
|
|
500
|
+
body: z.object({
|
|
501
|
+
// Use passthrough object for dynamic form data
|
|
502
|
+
data: z.object({}).passthrough(),
|
|
503
|
+
}),
|
|
504
|
+
},
|
|
505
|
+
async (ctx) => {
|
|
506
|
+
const { slug } = ctx.params;
|
|
507
|
+
const { data } = ctx.body;
|
|
508
|
+
const baseContext = createContext(ctx.headers);
|
|
509
|
+
|
|
510
|
+
// Get form by slug
|
|
511
|
+
const form = await adapter.findOne<Form>({
|
|
512
|
+
model: "form",
|
|
513
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
if (!form) {
|
|
517
|
+
throw ctx.error(404, { message: "Form not found" });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check if form is active
|
|
521
|
+
if (form.status !== "active") {
|
|
522
|
+
throw ctx.error(400, {
|
|
523
|
+
message: "Form is not accepting submissions",
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const submissionContext = createSubmissionContext(
|
|
528
|
+
slug,
|
|
529
|
+
form.id,
|
|
530
|
+
ctx.headers,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Validate data against form schema
|
|
534
|
+
// Use formSchemaToZod for consistent validation with the client-side,
|
|
535
|
+
// which properly handles date constraints and step metadata
|
|
536
|
+
try {
|
|
537
|
+
const jsonSchema = JSON.parse(form.schema);
|
|
538
|
+
const zodSchema = formSchemaToZod(jsonSchema);
|
|
539
|
+
const validation = zodSchema.safeParse(data);
|
|
540
|
+
if (!validation.success) {
|
|
541
|
+
throw ctx.error(400, {
|
|
542
|
+
message: "Validation failed",
|
|
543
|
+
errors: validation.error.issues,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
548
|
+
throw error; // Re-throw API errors
|
|
549
|
+
}
|
|
550
|
+
throw ctx.error(400, { message: "Invalid form data" });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Call before submission hook - may modify data or deny
|
|
554
|
+
let finalData = data as Record<string, unknown>;
|
|
555
|
+
if (config.hooks?.onBeforeSubmission) {
|
|
556
|
+
try {
|
|
557
|
+
const result = await config.hooks.onBeforeSubmission(
|
|
558
|
+
slug,
|
|
559
|
+
data as Record<string, unknown>,
|
|
560
|
+
submissionContext,
|
|
561
|
+
);
|
|
562
|
+
if (result === false) {
|
|
563
|
+
throw ctx.error(400, { message: "Submission rejected" });
|
|
564
|
+
}
|
|
565
|
+
if (result && typeof result === "object") {
|
|
566
|
+
finalData = result;
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
// Call error hook if submission is rejected
|
|
570
|
+
if (config.hooks?.onSubmissionError) {
|
|
571
|
+
await config.hooks.onSubmissionError(
|
|
572
|
+
error as Error,
|
|
573
|
+
slug,
|
|
574
|
+
data as Record<string, unknown>,
|
|
575
|
+
submissionContext,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Create submission
|
|
583
|
+
const submission = await adapter.create<FormSubmission>({
|
|
584
|
+
model: "formSubmission",
|
|
585
|
+
data: {
|
|
586
|
+
formId: form.id,
|
|
587
|
+
data: JSON.stringify(finalData),
|
|
588
|
+
submittedAt: new Date(),
|
|
589
|
+
ipAddress: baseContext.ipAddress,
|
|
590
|
+
userAgent: baseContext.userAgent,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const serialized = serializeFormSubmission(submission);
|
|
595
|
+
|
|
596
|
+
// Call after submission hook
|
|
597
|
+
if (config.hooks?.onAfterSubmission) {
|
|
598
|
+
await config.hooks.onAfterSubmission(
|
|
599
|
+
serialized,
|
|
600
|
+
serializeForm(form),
|
|
601
|
+
submissionContext,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
...serialized,
|
|
607
|
+
form: {
|
|
608
|
+
successMessage: form.successMessage,
|
|
609
|
+
redirectUrl: form.redirectUrl,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// ========== Submissions Management Endpoints (Admin) ==========
|
|
616
|
+
|
|
617
|
+
const listSubmissions = createEndpoint(
|
|
618
|
+
"/forms/:formId/submissions",
|
|
619
|
+
{
|
|
620
|
+
method: "GET",
|
|
621
|
+
params: z.object({ formId: z.string() }),
|
|
622
|
+
query: listSubmissionsQuerySchema,
|
|
623
|
+
},
|
|
624
|
+
async (ctx) => {
|
|
625
|
+
const { formId } = ctx.params;
|
|
626
|
+
const { limit, offset } = ctx.query;
|
|
627
|
+
const context = createContext(ctx.headers);
|
|
628
|
+
|
|
629
|
+
// Verify form exists
|
|
630
|
+
const form = await adapter.findOne<Form>({
|
|
631
|
+
model: "form",
|
|
632
|
+
where: [{ field: "id", value: formId, operator: "eq" as const }],
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (!form) {
|
|
636
|
+
throw ctx.error(404, { message: "Form not found" });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Call before hook for auth check
|
|
640
|
+
if (config.hooks?.onBeforeListSubmissions) {
|
|
641
|
+
const canList = await config.hooks.onBeforeListSubmissions(
|
|
642
|
+
formId,
|
|
643
|
+
context,
|
|
644
|
+
);
|
|
645
|
+
if (!canList) {
|
|
646
|
+
throw ctx.error(403, { message: "Access denied" });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Get total count
|
|
651
|
+
const allSubmissions = await adapter.findMany<FormSubmission>({
|
|
652
|
+
model: "formSubmission",
|
|
653
|
+
where: [
|
|
654
|
+
{ field: "formId", value: formId, operator: "eq" as const },
|
|
655
|
+
],
|
|
656
|
+
});
|
|
657
|
+
const total = allSubmissions.length;
|
|
658
|
+
|
|
659
|
+
// Get paginated submissions
|
|
660
|
+
const submissions = await adapter.findMany<FormSubmissionWithForm>({
|
|
661
|
+
model: "formSubmission",
|
|
662
|
+
where: [
|
|
663
|
+
{ field: "formId", value: formId, operator: "eq" as const },
|
|
664
|
+
],
|
|
665
|
+
limit,
|
|
666
|
+
offset,
|
|
667
|
+
sortBy: { field: "submittedAt", direction: "desc" },
|
|
668
|
+
join: { form: true },
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
items: submissions.map(serializeFormSubmissionWithData),
|
|
673
|
+
total,
|
|
674
|
+
limit,
|
|
675
|
+
offset,
|
|
676
|
+
};
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const getSubmission = createEndpoint(
|
|
681
|
+
"/forms/:formId/submissions/:subId",
|
|
682
|
+
{
|
|
683
|
+
method: "GET",
|
|
684
|
+
params: z.object({ formId: z.string(), subId: z.string() }),
|
|
685
|
+
},
|
|
686
|
+
async (ctx) => {
|
|
687
|
+
const { formId, subId } = ctx.params;
|
|
688
|
+
const context = createContext(ctx.headers);
|
|
689
|
+
|
|
690
|
+
// Call before hook for access check
|
|
691
|
+
if (config.hooks?.onBeforeGetSubmission) {
|
|
692
|
+
const canGet = await config.hooks.onBeforeGetSubmission(
|
|
693
|
+
subId,
|
|
694
|
+
context,
|
|
695
|
+
);
|
|
696
|
+
if (!canGet) {
|
|
697
|
+
throw ctx.error(403, { message: "Access denied" });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const submission = await adapter.findOne<FormSubmissionWithForm>({
|
|
702
|
+
model: "formSubmission",
|
|
703
|
+
where: [{ field: "id", value: subId, operator: "eq" as const }],
|
|
704
|
+
join: { form: true },
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (!submission || submission.formId !== formId) {
|
|
708
|
+
throw ctx.error(404, { message: "Submission not found" });
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return serializeFormSubmissionWithData(submission);
|
|
712
|
+
},
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const deleteSubmission = createEndpoint(
|
|
716
|
+
"/forms/:formId/submissions/:subId",
|
|
717
|
+
{
|
|
718
|
+
method: "DELETE",
|
|
719
|
+
params: z.object({ formId: z.string(), subId: z.string() }),
|
|
720
|
+
},
|
|
721
|
+
async (ctx) => {
|
|
722
|
+
const { formId, subId } = ctx.params;
|
|
723
|
+
const context = createContext(ctx.headers);
|
|
724
|
+
|
|
725
|
+
const existing = await adapter.findOne<FormSubmission>({
|
|
726
|
+
model: "formSubmission",
|
|
727
|
+
where: [{ field: "id", value: subId, operator: "eq" as const }],
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
if (!existing || existing.formId !== formId) {
|
|
731
|
+
throw ctx.error(404, { message: "Submission not found" });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Call before hook
|
|
735
|
+
if (config.hooks?.onBeforeSubmissionDeleted) {
|
|
736
|
+
const canDelete = await config.hooks.onBeforeSubmissionDeleted(
|
|
737
|
+
subId,
|
|
738
|
+
context,
|
|
739
|
+
);
|
|
740
|
+
if (!canDelete) {
|
|
741
|
+
throw ctx.error(403, { message: "Delete operation denied" });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
await adapter.delete({
|
|
746
|
+
model: "formSubmission",
|
|
747
|
+
where: [{ field: "id", value: subId, operator: "eq" as const }],
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// Call after hook
|
|
751
|
+
if (config.hooks?.onAfterSubmissionDeleted) {
|
|
752
|
+
await config.hooks.onAfterSubmissionDeleted(subId, context);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return { success: true };
|
|
756
|
+
},
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
listForms,
|
|
761
|
+
getFormBySlug,
|
|
762
|
+
getFormById,
|
|
763
|
+
createForm,
|
|
764
|
+
updateForm,
|
|
765
|
+
deleteForm,
|
|
766
|
+
submitForm,
|
|
767
|
+
listSubmissions,
|
|
768
|
+
getSubmission,
|
|
769
|
+
deleteSubmission,
|
|
770
|
+
};
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
export type FormBuilderApiRouter = ReturnType<
|
|
775
|
+
ReturnType<typeof formBuilderBackendPlugin>["routes"]
|
|
776
|
+
>;
|