@frigear-nu/forms 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +22 -2
  2. package/dist/module.d.mts +281 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +102 -0
  5. package/dist/runtime/app/components/FormField.d.vue.ts +16 -0
  6. package/dist/runtime/app/components/FormField.vue +80 -0
  7. package/dist/runtime/app/components/FormField.vue.d.ts +16 -0
  8. package/dist/runtime/app/components/FormRenderer.d.vue.ts +19 -0
  9. package/dist/runtime/app/components/FormRenderer.vue +145 -0
  10. package/dist/runtime/app/components/FormRenderer.vue.d.ts +19 -0
  11. package/dist/runtime/app/components/FormSubmissionView.d.vue.ts +7 -0
  12. package/dist/runtime/app/components/FormSubmissionView.vue +77 -0
  13. package/dist/runtime/app/components/FormSubmissionView.vue.d.ts +7 -0
  14. package/dist/runtime/app/composables/index.d.ts +2 -0
  15. package/dist/runtime/app/composables/index.js +2 -0
  16. package/dist/runtime/app/composables/useFormStepper.d.ts +21 -0
  17. package/dist/runtime/app/composables/useFormStepper.js +33 -0
  18. package/dist/runtime/app/composables/useForms.d.ts +21 -0
  19. package/dist/runtime/app/composables/useForms.js +52 -0
  20. package/dist/runtime/app/pages/admin-form.d.vue.ts +3 -0
  21. package/dist/runtime/app/pages/admin-form.vue +150 -0
  22. package/dist/runtime/app/pages/admin-form.vue.d.ts +3 -0
  23. package/dist/runtime/app/pages/admin-submission.d.vue.ts +3 -0
  24. package/dist/runtime/app/pages/admin-submission.vue +59 -0
  25. package/dist/runtime/app/pages/admin-submission.vue.d.ts +3 -0
  26. package/dist/runtime/app/pages/admin-submissions.d.vue.ts +3 -0
  27. package/dist/runtime/app/pages/admin-submissions.vue +71 -0
  28. package/dist/runtime/app/pages/admin-submissions.vue.d.ts +3 -0
  29. package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
  30. package/dist/runtime/app/pages/admin.vue +194 -0
  31. package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
  32. package/dist/runtime/app/pages/form.d.vue.ts +3 -0
  33. package/dist/runtime/app/pages/form.vue +18 -0
  34. package/dist/runtime/app/pages/form.vue.d.ts +3 -0
  35. package/dist/runtime/i18n/lang/da.json +84 -0
  36. package/dist/runtime/i18n/lang/en.json +84 -0
  37. package/dist/runtime/menus.d.ts +2 -0
  38. package/dist/runtime/menus.js +15 -0
  39. package/dist/runtime/server/db/schema.sqlite.d.ts +504 -0
  40. package/dist/runtime/server/db/schema.sqlite.js +46 -0
  41. package/dist/runtime/server/plugins/email-delivery.d.ts +2 -0
  42. package/dist/runtime/server/plugins/email-delivery.js +21 -0
  43. package/dist/runtime/server/plugins/seed.d.ts +2 -0
  44. package/dist/runtime/server/plugins/seed.js +11 -0
  45. package/dist/runtime/server/routes/admin/form.delete.d.ts +2 -0
  46. package/dist/runtime/server/routes/admin/form.delete.js +8 -0
  47. package/dist/runtime/server/routes/admin/form.get.d.ts +2 -0
  48. package/dist/runtime/server/routes/admin/form.get.js +4 -0
  49. package/dist/runtime/server/routes/admin/form.patch.d.ts +2 -0
  50. package/dist/runtime/server/routes/admin/form.patch.js +17 -0
  51. package/dist/runtime/server/routes/admin/forms.get.d.ts +2 -0
  52. package/dist/runtime/server/routes/admin/forms.get.js +19 -0
  53. package/dist/runtime/server/routes/admin/forms.post.d.ts +2 -0
  54. package/dist/runtime/server/routes/admin/forms.post.js +13 -0
  55. package/dist/runtime/server/routes/admin/import.post.d.ts +2 -0
  56. package/dist/runtime/server/routes/admin/import.post.js +6 -0
  57. package/dist/runtime/server/routes/admin/submission-file.get.d.ts +2 -0
  58. package/dist/runtime/server/routes/admin/submission-file.get.js +17 -0
  59. package/dist/runtime/server/routes/admin/submission-redeliver.post.d.ts +2 -0
  60. package/dist/runtime/server/routes/admin/submission-redeliver.post.js +29 -0
  61. package/dist/runtime/server/routes/admin/submission.get.d.ts +2 -0
  62. package/dist/runtime/server/routes/admin/submission.get.js +22 -0
  63. package/dist/runtime/server/routes/admin/submissions.get.d.ts +2 -0
  64. package/dist/runtime/server/routes/admin/submissions.get.js +16 -0
  65. package/dist/runtime/server/routes/public/form.get.d.ts +2 -0
  66. package/dist/runtime/server/routes/public/form.get.js +3 -0
  67. package/dist/runtime/server/routes/public/submit.post.d.ts +2 -0
  68. package/dist/runtime/server/routes/public/submit.post.js +7 -0
  69. package/dist/runtime/server/utils/admin-schemas.d.ts +291 -0
  70. package/dist/runtime/server/utils/admin-schemas.js +14 -0
  71. package/dist/runtime/server/utils/auth.d.ts +11 -0
  72. package/dist/runtime/server/utils/auth.js +7 -0
  73. package/dist/runtime/server/utils/forms.d.ts +9 -0
  74. package/dist/runtime/server/utils/forms.js +30 -0
  75. package/dist/runtime/server/utils/rate-limit.d.ts +6 -0
  76. package/dist/runtime/server/utils/rate-limit.js +14 -0
  77. package/dist/runtime/server/utils/render.d.ts +11 -0
  78. package/dist/runtime/server/utils/render.js +26 -0
  79. package/dist/runtime/server/utils/seed.d.ts +9 -0
  80. package/dist/runtime/server/utils/seed.js +22 -0
  81. package/dist/runtime/server/utils/submit.d.ts +9 -0
  82. package/dist/runtime/server/utils/submit.js +120 -0
  83. package/dist/runtime/utils/conditions.d.ts +19 -0
  84. package/dist/runtime/utils/conditions.js +36 -0
  85. package/dist/runtime/utils/definition.d.ts +91 -0
  86. package/dist/runtime/utils/definition.js +103 -0
  87. package/dist/runtime/utils/index.d.ts +13 -0
  88. package/dist/runtime/utils/index.js +8 -0
  89. package/dist/runtime/utils/markdown.d.ts +5 -0
  90. package/dist/runtime/utils/markdown.js +5 -0
  91. package/dist/runtime/utils/schema.d.ts +27 -0
  92. package/dist/runtime/utils/schema.js +95 -0
  93. package/dist/runtime/utils/text.d.ts +12 -0
  94. package/dist/runtime/utils/text.js +12 -0
  95. package/dist/types.d.mts +3 -0
  96. package/package.json +86 -3
@@ -0,0 +1,6 @@
1
+ import { importInput } from "../../utils/admin-schemas.js";
2
+ export default defineEventHandler(async (event) => {
3
+ await requireFormsManager(event);
4
+ const { forms } = await readValidatedBody(event, importInput.parse);
5
+ return seedForms(forms.map((form) => ({ ...form, definition: form.definition })));
6
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import { db, schema } from "@nuxthub/db";
3
+ import { blob } from "@nuxthub/blob";
4
+ export default defineEventHandler(async (event) => {
5
+ await requireFormsManager(event);
6
+ const [file] = await db.select().from(schema.formSubmissionFiles).where(and(
7
+ eq(schema.formSubmissionFiles.submissionId, getRouterParam(event, "id")),
8
+ eq(schema.formSubmissionFiles.id, getRouterParam(event, "fileId"))
9
+ )).limit(1);
10
+ if (!file) throw createError({ statusCode: 404, statusMessage: "File not found." });
11
+ const stored = await blob.get(file.pathname);
12
+ if (!stored) throw createError({ statusCode: 404, statusMessage: "File not found." });
13
+ setResponseHeader(event, "content-type", file.contentType);
14
+ setResponseHeader(event, "content-disposition", `attachment; filename="${encodeURIComponent(file.filename)}"`);
15
+ setResponseHeader(event, "x-content-type-options", "nosniff");
16
+ return stored;
17
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db, schema } from "@nuxthub/db";
3
+ export default defineEventHandler(async (event) => {
4
+ await requireFormsManager(event);
5
+ const [row] = await db.select().from(schema.formSubmissions).where(eq(schema.formSubmissions.id, getRouterParam(event, "id"))).limit(1);
6
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Submission not found." });
7
+ const files = await db.select().from(schema.formSubmissionFiles).where(eq(schema.formSubmissionFiles.submissionId, row.id));
8
+ const [form] = await db.select().from(schema.forms).where(eq(schema.forms.id, row.formId)).limit(1);
9
+ const ctx = {
10
+ event,
11
+ form: { id: row.formId, slug: row.formSlug, name: form?.name ?? row.formSlug, version: row.formVersion, definition: row.definitionSnapshot },
12
+ answers: row.answers,
13
+ files: files.map((file) => ({ fieldKey: file.fieldKey, filename: file.filename, contentType: file.contentType, size: file.size, pathname: file.pathname })),
14
+ locale: row.locale,
15
+ userId: row.userId,
16
+ submissionId: row.id,
17
+ stored: true,
18
+ delivered: false
19
+ };
20
+ let error;
21
+ try {
22
+ await useNitroApp().hooks.callHook("forms:submitted", ctx);
23
+ } catch (cause) {
24
+ error = cause instanceof Error ? cause.message : String(cause);
25
+ }
26
+ const deliveryStatus = error ? "failed" : ctx.delivered ? "delivered" : "none";
27
+ await db.update(schema.formSubmissions).set({ deliveryStatus, deliveryError: error ?? null }).where(eq(schema.formSubmissions.id, row.id));
28
+ return { deliveryStatus, deliveryError: error ?? null };
29
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db, schema } from "@nuxthub/db";
3
+ export default defineEventHandler(async (event) => {
4
+ await requireFormsManager(event);
5
+ const [row] = await db.select().from(schema.formSubmissions).where(eq(schema.formSubmissions.id, getRouterParam(event, "id"))).limit(1);
6
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Submission not found." });
7
+ const files = await db.select().from(schema.formSubmissionFiles).where(eq(schema.formSubmissionFiles.submissionId, row.id));
8
+ return {
9
+ id: row.id,
10
+ formId: row.formId,
11
+ formSlug: row.formSlug,
12
+ formVersion: row.formVersion,
13
+ definition: row.definitionSnapshot,
14
+ answers: row.answers,
15
+ files: files.map(({ id, fieldKey, filename, contentType, size }) => ({ id, fieldKey, filename, contentType, size })),
16
+ locale: row.locale,
17
+ userId: row.userId,
18
+ deliveryStatus: row.deliveryStatus,
19
+ deliveryError: row.deliveryError,
20
+ createdAt: row.createdAt.toISOString()
21
+ };
22
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { desc, eq } from "drizzle-orm";
2
+ import { db, schema } from "@nuxthub/db";
3
+ import { renderAnswers } from "../../utils/render.js";
4
+ export default defineEventHandler(async (event) => {
5
+ await requireFormsManager(event);
6
+ const form = await findFormById(getRouterParam(event, "id"));
7
+ const rows = await db.select().from(schema.formSubmissions).where(eq(schema.formSubmissions.formId, form.id)).orderBy(desc(schema.formSubmissions.createdAt)).limit(500);
8
+ return rows.map((row) => ({
9
+ id: row.id,
10
+ createdAt: row.createdAt.toISOString(),
11
+ locale: row.locale,
12
+ userId: row.userId,
13
+ deliveryStatus: row.deliveryStatus,
14
+ preview: renderAnswers(row.definitionSnapshot, row.answers, row.locale).filter((answer) => answer.value).slice(0, 3).map(({ label, value }) => ({ label, value: value.slice(0, 120) }))
15
+ }));
16
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export default defineEventHandler(async (event) => {
2
+ return toPublicForm(await findPublishedForm(getRouterParam(event, "slug")));
3
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,7 @@
1
+ export default defineEventHandler(async (event) => {
2
+ const slug = getRouterParam(event, "slug");
3
+ await enforceRateLimit(event, slug);
4
+ const form = await findPublishedForm(slug);
5
+ const session = await getUserSession(event);
6
+ return processSubmission(event, form, session.user?.id ?? null);
7
+ });
@@ -0,0 +1,291 @@
1
+ import { z } from 'zod';
2
+ export declare const formInput: z.ZodObject<{
3
+ slug: z.ZodString;
4
+ name: z.ZodString;
5
+ status: z.ZodDefault<z.ZodEnum<{
6
+ draft: "draft";
7
+ published: "published";
8
+ archived: "archived";
9
+ }>>;
10
+ definition: z.ZodObject<{
11
+ mode: z.ZodEnum<{
12
+ single: "single";
13
+ stepped: "stepped";
14
+ }>;
15
+ storage: z.ZodDefault<z.ZodEnum<{
16
+ db: "db";
17
+ none: "none";
18
+ }>>;
19
+ steps: z.ZodArray<z.ZodObject<{
20
+ id: z.ZodString;
21
+ title: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
22
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
23
+ showIf: z.ZodOptional<z.ZodObject<{
24
+ field: z.ZodString;
25
+ op: z.ZodEnum<{
26
+ eq: "eq";
27
+ neq: "neq";
28
+ in: "in";
29
+ truthy: "truthy";
30
+ }>;
31
+ value: z.ZodOptional<z.ZodUnknown>;
32
+ }, z.core.$strip>>;
33
+ fields: z.ZodArray<z.ZodObject<{
34
+ key: z.ZodString;
35
+ type: z.ZodEnum<{
36
+ number: "number";
37
+ text: "text";
38
+ email: "email";
39
+ tel: "tel";
40
+ textarea: "textarea";
41
+ select: "select";
42
+ radio: "radio";
43
+ checkbox: "checkbox";
44
+ date: "date";
45
+ file: "file";
46
+ markdown: "markdown";
47
+ }>;
48
+ content: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
49
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
50
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
51
+ placeholder: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
52
+ required: z.ZodOptional<z.ZodBoolean>;
53
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
+ value: z.ZodString;
55
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
56
+ }, z.core.$strip>>>;
57
+ multiple: z.ZodOptional<z.ZodBoolean>;
58
+ autocomplete: z.ZodOptional<z.ZodString>;
59
+ validation: z.ZodOptional<z.ZodObject<{
60
+ min: z.ZodOptional<z.ZodNumber>;
61
+ max: z.ZodOptional<z.ZodNumber>;
62
+ minLength: z.ZodOptional<z.ZodNumber>;
63
+ maxLength: z.ZodOptional<z.ZodNumber>;
64
+ pattern: z.ZodOptional<z.ZodString>;
65
+ maxFiles: z.ZodOptional<z.ZodNumber>;
66
+ maxSize: z.ZodOptional<z.ZodNumber>;
67
+ accept: z.ZodOptional<z.ZodArray<z.ZodString>>;
68
+ message: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
69
+ }, z.core.$strip>>;
70
+ showIf: z.ZodOptional<z.ZodObject<{
71
+ field: z.ZodString;
72
+ op: z.ZodEnum<{
73
+ eq: "eq";
74
+ neq: "neq";
75
+ in: "in";
76
+ truthy: "truthy";
77
+ }>;
78
+ value: z.ZodOptional<z.ZodUnknown>;
79
+ }, z.core.$strip>>;
80
+ }, z.core.$strip>>;
81
+ }, z.core.$strip>>;
82
+ submit: z.ZodOptional<z.ZodObject<{
83
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
84
+ successMessage: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
85
+ redirect: z.ZodOptional<z.ZodString>;
86
+ allowResubmit: z.ZodOptional<z.ZodObject<{
87
+ keep: z.ZodArray<z.ZodString>;
88
+ }, z.core.$strip>>;
89
+ }, z.core.$strip>>;
90
+ delivery: z.ZodOptional<z.ZodObject<{
91
+ email: z.ZodOptional<z.ZodObject<{
92
+ to: z.ZodArray<z.ZodString>;
93
+ subject: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
94
+ }, z.core.$strip>>;
95
+ }, z.core.$strip>>;
96
+ }, z.core.$strip>;
97
+ }, z.core.$strip>;
98
+ export declare const formPatch: z.ZodObject<{
99
+ slug: z.ZodOptional<z.ZodString>;
100
+ name: z.ZodOptional<z.ZodString>;
101
+ definition: z.ZodOptional<z.ZodObject<{
102
+ mode: z.ZodEnum<{
103
+ single: "single";
104
+ stepped: "stepped";
105
+ }>;
106
+ storage: z.ZodDefault<z.ZodEnum<{
107
+ db: "db";
108
+ none: "none";
109
+ }>>;
110
+ steps: z.ZodArray<z.ZodObject<{
111
+ id: z.ZodString;
112
+ title: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
113
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
114
+ showIf: z.ZodOptional<z.ZodObject<{
115
+ field: z.ZodString;
116
+ op: z.ZodEnum<{
117
+ eq: "eq";
118
+ neq: "neq";
119
+ in: "in";
120
+ truthy: "truthy";
121
+ }>;
122
+ value: z.ZodOptional<z.ZodUnknown>;
123
+ }, z.core.$strip>>;
124
+ fields: z.ZodArray<z.ZodObject<{
125
+ key: z.ZodString;
126
+ type: z.ZodEnum<{
127
+ number: "number";
128
+ text: "text";
129
+ email: "email";
130
+ tel: "tel";
131
+ textarea: "textarea";
132
+ select: "select";
133
+ radio: "radio";
134
+ checkbox: "checkbox";
135
+ date: "date";
136
+ file: "file";
137
+ markdown: "markdown";
138
+ }>;
139
+ content: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
140
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
141
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
142
+ placeholder: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
143
+ required: z.ZodOptional<z.ZodBoolean>;
144
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
145
+ value: z.ZodString;
146
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
147
+ }, z.core.$strip>>>;
148
+ multiple: z.ZodOptional<z.ZodBoolean>;
149
+ autocomplete: z.ZodOptional<z.ZodString>;
150
+ validation: z.ZodOptional<z.ZodObject<{
151
+ min: z.ZodOptional<z.ZodNumber>;
152
+ max: z.ZodOptional<z.ZodNumber>;
153
+ minLength: z.ZodOptional<z.ZodNumber>;
154
+ maxLength: z.ZodOptional<z.ZodNumber>;
155
+ pattern: z.ZodOptional<z.ZodString>;
156
+ maxFiles: z.ZodOptional<z.ZodNumber>;
157
+ maxSize: z.ZodOptional<z.ZodNumber>;
158
+ accept: z.ZodOptional<z.ZodArray<z.ZodString>>;
159
+ message: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
160
+ }, z.core.$strip>>;
161
+ showIf: z.ZodOptional<z.ZodObject<{
162
+ field: z.ZodString;
163
+ op: z.ZodEnum<{
164
+ eq: "eq";
165
+ neq: "neq";
166
+ in: "in";
167
+ truthy: "truthy";
168
+ }>;
169
+ value: z.ZodOptional<z.ZodUnknown>;
170
+ }, z.core.$strip>>;
171
+ }, z.core.$strip>>;
172
+ }, z.core.$strip>>;
173
+ submit: z.ZodOptional<z.ZodObject<{
174
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
175
+ successMessage: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
176
+ redirect: z.ZodOptional<z.ZodString>;
177
+ allowResubmit: z.ZodOptional<z.ZodObject<{
178
+ keep: z.ZodArray<z.ZodString>;
179
+ }, z.core.$strip>>;
180
+ }, z.core.$strip>>;
181
+ delivery: z.ZodOptional<z.ZodObject<{
182
+ email: z.ZodOptional<z.ZodObject<{
183
+ to: z.ZodArray<z.ZodString>;
184
+ subject: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
185
+ }, z.core.$strip>>;
186
+ }, z.core.$strip>>;
187
+ }, z.core.$strip>>;
188
+ status: z.ZodOptional<z.ZodEnum<{
189
+ draft: "draft";
190
+ published: "published";
191
+ archived: "archived";
192
+ }>>;
193
+ }, z.core.$strip>;
194
+ export declare const importInput: z.ZodObject<{
195
+ forms: z.ZodArray<z.ZodObject<{
196
+ slug: z.ZodString;
197
+ name: z.ZodString;
198
+ definition: z.ZodObject<{
199
+ mode: z.ZodEnum<{
200
+ single: "single";
201
+ stepped: "stepped";
202
+ }>;
203
+ storage: z.ZodDefault<z.ZodEnum<{
204
+ db: "db";
205
+ none: "none";
206
+ }>>;
207
+ steps: z.ZodArray<z.ZodObject<{
208
+ id: z.ZodString;
209
+ title: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
210
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
211
+ showIf: z.ZodOptional<z.ZodObject<{
212
+ field: z.ZodString;
213
+ op: z.ZodEnum<{
214
+ eq: "eq";
215
+ neq: "neq";
216
+ in: "in";
217
+ truthy: "truthy";
218
+ }>;
219
+ value: z.ZodOptional<z.ZodUnknown>;
220
+ }, z.core.$strip>>;
221
+ fields: z.ZodArray<z.ZodObject<{
222
+ key: z.ZodString;
223
+ type: z.ZodEnum<{
224
+ number: "number";
225
+ text: "text";
226
+ email: "email";
227
+ tel: "tel";
228
+ textarea: "textarea";
229
+ select: "select";
230
+ radio: "radio";
231
+ checkbox: "checkbox";
232
+ date: "date";
233
+ file: "file";
234
+ markdown: "markdown";
235
+ }>;
236
+ content: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
237
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
238
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
239
+ placeholder: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
240
+ required: z.ZodOptional<z.ZodBoolean>;
241
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
242
+ value: z.ZodString;
243
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
244
+ }, z.core.$strip>>>;
245
+ multiple: z.ZodOptional<z.ZodBoolean>;
246
+ autocomplete: z.ZodOptional<z.ZodString>;
247
+ validation: z.ZodOptional<z.ZodObject<{
248
+ min: z.ZodOptional<z.ZodNumber>;
249
+ max: z.ZodOptional<z.ZodNumber>;
250
+ minLength: z.ZodOptional<z.ZodNumber>;
251
+ maxLength: z.ZodOptional<z.ZodNumber>;
252
+ pattern: z.ZodOptional<z.ZodString>;
253
+ maxFiles: z.ZodOptional<z.ZodNumber>;
254
+ maxSize: z.ZodOptional<z.ZodNumber>;
255
+ accept: z.ZodOptional<z.ZodArray<z.ZodString>>;
256
+ message: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
257
+ }, z.core.$strip>>;
258
+ showIf: z.ZodOptional<z.ZodObject<{
259
+ field: z.ZodString;
260
+ op: z.ZodEnum<{
261
+ eq: "eq";
262
+ neq: "neq";
263
+ in: "in";
264
+ truthy: "truthy";
265
+ }>;
266
+ value: z.ZodOptional<z.ZodUnknown>;
267
+ }, z.core.$strip>>;
268
+ }, z.core.$strip>>;
269
+ }, z.core.$strip>>;
270
+ submit: z.ZodOptional<z.ZodObject<{
271
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
272
+ successMessage: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
273
+ redirect: z.ZodOptional<z.ZodString>;
274
+ allowResubmit: z.ZodOptional<z.ZodObject<{
275
+ keep: z.ZodArray<z.ZodString>;
276
+ }, z.core.$strip>>;
277
+ }, z.core.$strip>>;
278
+ delivery: z.ZodOptional<z.ZodObject<{
279
+ email: z.ZodOptional<z.ZodObject<{
280
+ to: z.ZodArray<z.ZodString>;
281
+ subject: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
282
+ }, z.core.$strip>>;
283
+ }, z.core.$strip>>;
284
+ }, z.core.$strip>;
285
+ status: z.ZodOptional<z.ZodEnum<{
286
+ draft: "draft";
287
+ published: "published";
288
+ archived: "archived";
289
+ }>>;
290
+ }, z.core.$strip>>;
291
+ }, z.core.$strip>;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { formDefinitionSchema } from "../../utils/index.js";
3
+ const slug = z.string().min(1).max(100).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, digits and dashes.").refine((value) => value !== "admin", "This slug is reserved.");
4
+ const statusSchema = z.enum(["draft", "published", "archived"]);
5
+ export const formInput = z.object({
6
+ slug,
7
+ name: z.string().min(1).max(200),
8
+ status: statusSchema.default("draft"),
9
+ definition: formDefinitionSchema
10
+ });
11
+ export const formPatch = formInput.partial().extend({ status: statusSchema.optional() });
12
+ export const importInput = z.object({
13
+ forms: z.array(formInput.extend({ status: statusSchema.optional() })).min(1).max(50)
14
+ });
@@ -0,0 +1,11 @@
1
+ import type { H3Event } from 'h3';
2
+ /**
3
+ * Gate for the admin routes (form definitions, submissions, files). This repo has no shared
4
+ * permission system, so the host app decides through the `forms:authorize` Nitro hook.
5
+ * Denied by default:
6
+ *
7
+ * nitroApp.hooks.hook('forms:authorize', (ctx) => { ctx.allowed = isStaff(ctx.userId) })
8
+ */
9
+ export declare function requireFormsManager(event: H3Event): Promise<{
10
+ userId: number;
11
+ }>;
@@ -0,0 +1,7 @@
1
+ export async function requireFormsManager(event) {
2
+ const { user } = await requireUserSession(event);
3
+ const ctx = { event, userId: user.id, allowed: false };
4
+ await useNitroApp().hooks.callHook("forms:authorize", ctx);
5
+ if (!ctx.allowed) throw createError({ statusCode: 403, statusMessage: "You are not allowed to manage forms." });
6
+ return { userId: user.id };
7
+ }
@@ -0,0 +1,9 @@
1
+ import { schema } from '@nuxthub/db';
2
+ import type { FormAdminDto, PublicFormDto } from '../../../types.js';
3
+ type FormRow = typeof schema.forms.$inferSelect;
4
+ export declare const toPublicForm: (row: FormRow) => PublicFormDto;
5
+ export declare const toAdminForm: (row: FormRow) => FormAdminDto;
6
+ export declare function findFormById(id: string): Promise<FormRow>;
7
+ export declare function findFormBySlug(slug: string): Promise<FormRow | undefined>;
8
+ export declare function findPublishedForm(slug: string): Promise<FormRow>;
9
+ export {};
@@ -0,0 +1,30 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db, schema } from "@nuxthub/db";
3
+ export const toPublicForm = (row) => {
4
+ const { delivery: _delivery, ...definition } = row.definition;
5
+ return { id: row.id, slug: row.slug, name: row.name, version: row.version, definition };
6
+ };
7
+ export const toAdminForm = (row) => ({
8
+ id: row.id,
9
+ slug: row.slug,
10
+ name: row.name,
11
+ status: row.status,
12
+ version: row.version,
13
+ definition: row.definition,
14
+ createdAt: row.createdAt.toISOString(),
15
+ updatedAt: row.updatedAt.toISOString()
16
+ });
17
+ export async function findFormById(id) {
18
+ const [row] = await db.select().from(schema.forms).where(eq(schema.forms.id, id)).limit(1);
19
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Form not found." });
20
+ return row;
21
+ }
22
+ export async function findFormBySlug(slug) {
23
+ const [row] = await db.select().from(schema.forms).where(eq(schema.forms.slug, slug)).limit(1);
24
+ return row;
25
+ }
26
+ export async function findPublishedForm(slug) {
27
+ const row = await findFormBySlug(slug);
28
+ if (!row || row.status !== "published") throw createError({ statusCode: 404, statusMessage: "Form not found." });
29
+ return row;
30
+ }
@@ -0,0 +1,6 @@
1
+ import type { H3Event } from 'h3';
2
+ /**
3
+ * Fixed-window counter per client and form, kept in KV. Not exact under concurrency (read then
4
+ * write), which is fine for throttling casual abuse. `rateLimit: 0` disables it.
5
+ */
6
+ export declare function enforceRateLimit(event: H3Event, slug: string): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { kv } from "@nuxthub/kv";
2
+ export async function enforceRateLimit(event, slug) {
3
+ const { rateLimit, rateLimitWindowSeconds } = useRuntimeConfig().forms;
4
+ if (!rateLimit) return;
5
+ const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown";
6
+ const window = Math.floor(Date.now() / (rateLimitWindowSeconds * 1e3));
7
+ const key = `forms:rate:${slug}:${ip}:${window}`;
8
+ const used = Number(await kv.get(key) ?? 0);
9
+ if (used >= rateLimit) {
10
+ setResponseHeader(event, "retry-after", rateLimitWindowSeconds);
11
+ throw createError({ statusCode: 429, statusMessage: "Too many submissions, try again later." });
12
+ }
13
+ await kv.set(key, used + 1, { ttl: rateLimitWindowSeconds });
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { FormDefinition } from '../../../types.js';
2
+ export type RenderedAnswer = {
3
+ key: string;
4
+ label: string;
5
+ value: string;
6
+ };
7
+ /** Answers as label/value rows in form order, using the definition the answers were given against. */
8
+ export declare function renderAnswers(definition: FormDefinition, answers: Record<string, unknown>, locale: string): RenderedAnswer[];
9
+ export declare const escapeHtml: (value: string) => string;
10
+ export declare function answersToText(rows: RenderedAnswer[]): string;
11
+ export declare function answersToHtml(rows: RenderedAnswer[]): string;
@@ -0,0 +1,26 @@
1
+ import { isInputField, resolveVisibility, translate } from "../../utils/index.js";
2
+ const optionLabel = (field, value, locale) => translate(field.options?.find((option) => option.value === value)?.label, locale) || value;
3
+ function formatValue(field, value, locale) {
4
+ if (value === void 0 || value === null || value === "") return "";
5
+ if (field.type === "checkbox") return value === true ? "Yes" : "No";
6
+ if (field.type === "file") return Array.isArray(value) ? value.map((file) => file.name).join(", ") : "";
7
+ if ((field.type === "select" || field.type === "radio") && typeof value === "string") return optionLabel(field, value, locale);
8
+ return String(value);
9
+ }
10
+ export function renderAnswers(definition, answers, locale) {
11
+ return resolveVisibility(definition, answers).flatMap(({ fields }) => fields).filter(isInputField).map((field) => ({
12
+ key: field.key,
13
+ label: translate(field.label, locale) || field.key,
14
+ value: formatValue(field, answers[field.key], locale)
15
+ }));
16
+ }
17
+ export const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18
+ export function answersToText(rows) {
19
+ return rows.map((row) => `${row.label}: ${row.value || "-"}`).join("\n");
20
+ }
21
+ export function answersToHtml(rows) {
22
+ const body = rows.map(
23
+ (row) => `<tr><td style="padding:4px 12px 4px 0;vertical-align:top;font-weight:600">${escapeHtml(row.label)}</td><td style="padding:4px 0;white-space:pre-wrap">${escapeHtml(row.value) || "-"}</td></tr>`
24
+ ).join("");
25
+ return `<table style="border-collapse:collapse;font-family:sans-serif;font-size:14px">${body}</table>`;
26
+ }
@@ -0,0 +1,9 @@
1
+ import type { SeedForm } from '../../../types.js';
2
+ /**
3
+ * Inserts the given forms, skipping every slug that already exists. Never updates anything, so
4
+ * edits made in the admin are safe. Returns what was inserted and what was skipped.
5
+ */
6
+ export declare function seedForms(forms: SeedForm[]): Promise<{
7
+ inserted: string[];
8
+ skipped: string[];
9
+ }>;
@@ -0,0 +1,22 @@
1
+ import { inArray } from "drizzle-orm";
2
+ import { db, schema } from "@nuxthub/db";
3
+ import { newId, parseFormDefinition } from "../../utils/index.js";
4
+ export async function seedForms(forms) {
5
+ if (!forms.length) return { inserted: [], skipped: [] };
6
+ const existing = await db.select({ slug: schema.forms.slug }).from(schema.forms).where(inArray(schema.forms.slug, forms.map((form) => form.slug)));
7
+ const taken = new Set(existing.map((row) => row.slug));
8
+ const inserted = [];
9
+ for (const form of forms) {
10
+ if (taken.has(form.slug)) continue;
11
+ await db.insert(schema.forms).values({
12
+ id: newId("frm"),
13
+ slug: form.slug,
14
+ name: form.name,
15
+ status: form.status ?? "draft",
16
+ definition: parseFormDefinition(form.definition)
17
+ });
18
+ inserted.push(form.slug);
19
+ taken.add(form.slug);
20
+ }
21
+ return { inserted, skipped: forms.map((form) => form.slug).filter((slug) => !inserted.includes(slug)) };
22
+ }
@@ -0,0 +1,9 @@
1
+ import type { H3Event } from 'h3';
2
+ import { schema } from '@nuxthub/db';
3
+ type FormRow = typeof schema.forms.$inferSelect;
4
+ /** Validates and processes one submission for a published form. */
5
+ export declare function processSubmission(event: H3Event, form: FormRow, userId: number | null): Promise<{
6
+ ok: true;
7
+ id: string | null;
8
+ }>;
9
+ export {};