@aphexcms/plugin-forms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/compile.d.ts +75 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +119 -0
- package/dist/compile.js.map +1 -0
- package/dist/events.d.ts +18 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +19 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.d.ts +17 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +76 -0
- package/dist/notify.js.map +1 -0
- package/dist/schema.d.ts +14 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +492 -0
- package/dist/schema.js.map +1 -0
- package/dist/submit.d.ts +4 -0
- package/dist/submit.d.ts.map +1 -0
- package/dist/submit.js +213 -0
- package/dist/submit.js.map +1 -0
- package/dist/template.d.ts +36 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +51 -0
- package/dist/template.js.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @aphexcms/plugin-forms
|
|
2
|
+
|
|
3
|
+
Editor-composed forms for [AphexCMS](https://github.com/IcelandicIcecream/aphex) — contact forms, signups, surveys — built in the admin rather than in code, with submissions stored as documents.
|
|
4
|
+
|
|
5
|
+
📚 **Documentation: [docs.getaphex.com/forms](https://docs.getaphex.com/forms)**
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @aphexcms/plugin-forms
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts title="src/lib/plugins.ts"
|
|
14
|
+
import { formsPlugin } from '@aphexcms/plugin-forms';
|
|
15
|
+
|
|
16
|
+
export const plugins = [
|
|
17
|
+
formsPlugin({
|
|
18
|
+
// Address notification emails come from. Falls back to APHEX_EMAIL_FROM.
|
|
19
|
+
from: 'Acme <forms@example.com>'
|
|
20
|
+
})
|
|
21
|
+
];
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The plugin is self-contained: it registers its own schemas, its submission endpoint, its admin tool, and its notification consumer. Nothing else in `aphex.config.ts` needs to know it exists.
|
|
25
|
+
|
|
26
|
+
After adding it, regenerate types with the plugins path as the third argument, or the `form` and `formSubmission` collections stay untyped:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
aphex generate:types ./src/lib/schemaTypes/index.ts ./src/lib/generated-types.ts ./src/lib/plugins.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## What you get
|
|
33
|
+
|
|
34
|
+
- A **form builder** in the admin — fields, labels, options, required flags, and confirmation behaviour, all editable by a non-developer.
|
|
35
|
+
- **Stored submissions** as ordinary documents, so they're searchable, exportable, and subject to the same access control as everything else.
|
|
36
|
+
- **Validation** derived from the form definition and enforced server-side, not just in the browser.
|
|
37
|
+
- **Spam handling** — a honeypot field plus rate limiting on the submission endpoint.
|
|
38
|
+
- **Email notifications** via an event consumer, so a failed send retries with backoff instead of vanishing.
|
|
39
|
+
|
|
40
|
+
## Submissions carry personal data
|
|
41
|
+
|
|
42
|
+
They usually contain names and email addresses, so the default access rule on `formSubmission` is deliberately stricter than on ordinary content. Widen it knowingly, and check it before exposing submissions through the HTTP or GraphQL APIs.
|
|
43
|
+
|
|
44
|
+
## Multi-tenancy
|
|
45
|
+
|
|
46
|
+
The endpoint derives the organization from the globally unique published form ID and never accepts
|
|
47
|
+
an organization ID from the visitor. The submission and its notification event then commit in one
|
|
48
|
+
transaction within that exact tenant.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { FormDefinition } from '@aphexcms/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* Turn a `form` document — what an editor composed in the studio — into a
|
|
4
|
+
* `FormDefinition`, the developer-authored form shape cms-core already knows how
|
|
5
|
+
* to validate.
|
|
6
|
+
*
|
|
7
|
+
* This is the whole reason the plugin is small. `validateFormData` runs a form's
|
|
8
|
+
* fields through the same engine the admin uses for documents, so "required",
|
|
9
|
+
* "is an email", "within this range" and "one of these options" are already
|
|
10
|
+
* implemented, already consistent with the rest of the CMS, and already returning
|
|
11
|
+
* errors in the shape every other surface uses. Hand-rolling a second validator
|
|
12
|
+
* for submissions would be a second set of rules to keep in step — and the one
|
|
13
|
+
* that runs against untrusted input is the worst place to have a divergent copy.
|
|
14
|
+
*
|
|
15
|
+
* The two layers meet here: `defineForm` is a form a developer writes in code;
|
|
16
|
+
* a `form` document is a form an editor drew in the studio. Compiling the second
|
|
17
|
+
* into the first means only the authoring differs.
|
|
18
|
+
*/
|
|
19
|
+
/** One field as stored on a `form` document. */
|
|
20
|
+
export interface FormFieldValue {
|
|
21
|
+
_type: string;
|
|
22
|
+
_key?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
label?: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
width?: 'full' | 'half';
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
defaultValue?: string | boolean;
|
|
29
|
+
rows?: number;
|
|
30
|
+
min?: number;
|
|
31
|
+
max?: number;
|
|
32
|
+
options?: Array<{
|
|
33
|
+
label?: string;
|
|
34
|
+
value?: string;
|
|
35
|
+
}>;
|
|
36
|
+
message?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** A `form` document, as far as this plugin cares. */
|
|
39
|
+
export interface FormDocument {
|
|
40
|
+
id: string;
|
|
41
|
+
_meta?: {
|
|
42
|
+
publishedHash?: string | null;
|
|
43
|
+
status?: string | null;
|
|
44
|
+
};
|
|
45
|
+
title?: string;
|
|
46
|
+
fields?: FormFieldValue[];
|
|
47
|
+
submitButtonLabel?: string;
|
|
48
|
+
confirmationType?: 'message' | 'redirect';
|
|
49
|
+
confirmationMessage?: unknown;
|
|
50
|
+
redirectUrl?: string;
|
|
51
|
+
emails?: NotificationEmailValue[];
|
|
52
|
+
}
|
|
53
|
+
export interface NotificationEmailValue {
|
|
54
|
+
_type?: string;
|
|
55
|
+
_key?: string;
|
|
56
|
+
to?: string;
|
|
57
|
+
replyTo?: string;
|
|
58
|
+
subject?: string;
|
|
59
|
+
message?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Field kinds that collect an answer. `formMessage` is display-only. */
|
|
62
|
+
export declare function isInputField(field: FormFieldValue): boolean;
|
|
63
|
+
/** The stored value for a select option — its `value`, or the label when blank. */
|
|
64
|
+
export declare function optionValue(option: {
|
|
65
|
+
label?: string;
|
|
66
|
+
value?: string;
|
|
67
|
+
}): string;
|
|
68
|
+
/**
|
|
69
|
+
* Compile a `form` document into a `FormDefinition` that `validateFormData` can
|
|
70
|
+
* run. `store` and `notifyEmail` are deliberately left off — this plugin stores
|
|
71
|
+
* submissions as documents and sends notifications from its own consumer, so the
|
|
72
|
+
* definition is used purely as a validation contract.
|
|
73
|
+
*/
|
|
74
|
+
export declare function compileForm(doc: FormDocument): FormDefinition;
|
|
75
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../src/lib/compile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEhE;;;;;;;;;;;;;;;;GAgBG;AAEH,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IAC1C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE3D;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAE9E;AA+FD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,cAAc,CAkB7D"}
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Field kinds that collect an answer. `formMessage` is display-only. */
|
|
2
|
+
export function isInputField(field) {
|
|
3
|
+
return field._type !== 'formMessage' && typeof field.name === 'string' && field.name.length > 0;
|
|
4
|
+
}
|
|
5
|
+
/** The stored value for a select option — its `value`, or the label when blank. */
|
|
6
|
+
export function optionValue(option) {
|
|
7
|
+
return (option.value ?? '').trim() || (option.label ?? '').trim();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compile one authored field into a CMS `Field`.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` for anything that collects nothing, so the caller can filter
|
|
13
|
+
* rather than branch. An unrecognised `_type` also returns `null`: a form saved
|
|
14
|
+
* by a newer version of this plugin must not have an unknown field silently
|
|
15
|
+
* treated as free text.
|
|
16
|
+
*/
|
|
17
|
+
function toField(field) {
|
|
18
|
+
if (!isInputField(field))
|
|
19
|
+
return null;
|
|
20
|
+
const name = field.name;
|
|
21
|
+
const title = field.label ?? name;
|
|
22
|
+
const required = field.required === true;
|
|
23
|
+
switch (field._type) {
|
|
24
|
+
case 'formText':
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
type: 'string',
|
|
28
|
+
title,
|
|
29
|
+
validation: (Rule) => (required ? Rule.required() : Rule.optional())
|
|
30
|
+
};
|
|
31
|
+
case 'formEmail':
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
type: 'string',
|
|
35
|
+
title,
|
|
36
|
+
// `.email()` on an optional field only judges values that are present,
|
|
37
|
+
// so a blank optional email stays valid.
|
|
38
|
+
validation: (Rule) => (required ? Rule.required().email() : Rule.optional().email())
|
|
39
|
+
};
|
|
40
|
+
case 'formTextarea':
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
type: 'text',
|
|
44
|
+
title,
|
|
45
|
+
validation: (Rule) => (required ? Rule.required() : Rule.optional())
|
|
46
|
+
};
|
|
47
|
+
case 'formNumber':
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
type: 'number',
|
|
51
|
+
title,
|
|
52
|
+
validation: (Rule) => {
|
|
53
|
+
let rule = required ? Rule.required() : Rule.optional();
|
|
54
|
+
if (typeof field.min === 'number')
|
|
55
|
+
rule = rule.min(field.min);
|
|
56
|
+
if (typeof field.max === 'number')
|
|
57
|
+
rule = rule.max(field.max);
|
|
58
|
+
return rule;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
case 'formCheckbox':
|
|
62
|
+
return {
|
|
63
|
+
name,
|
|
64
|
+
type: 'boolean',
|
|
65
|
+
title,
|
|
66
|
+
/*
|
|
67
|
+
* "Required" on a checkbox means ticked, not present — it's the consent
|
|
68
|
+
* box. `Rule.required()` would accept `false`, which is exactly the case
|
|
69
|
+
* the editor is trying to prevent, so this is a custom rule instead.
|
|
70
|
+
*/
|
|
71
|
+
validation: (Rule) => required
|
|
72
|
+
? Rule.custom((value) => value === true || 'This must be ticked')
|
|
73
|
+
: Rule.optional()
|
|
74
|
+
};
|
|
75
|
+
case 'formSelect': {
|
|
76
|
+
const values = (field.options ?? []).map(optionValue).filter(Boolean);
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
type: 'string',
|
|
80
|
+
title,
|
|
81
|
+
list: values,
|
|
82
|
+
validation: (Rule) => (required ? Rule.required() : Rule.optional()).custom((value) => {
|
|
83
|
+
if (value === undefined || value === null || value === '')
|
|
84
|
+
return true;
|
|
85
|
+
// Never trust the posted value to be one the form offered: a select
|
|
86
|
+
// is a client-side constraint, and the request isn't.
|
|
87
|
+
return values.includes(String(value)) || 'Not one of the available options';
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Compile a `form` document into a `FormDefinition` that `validateFormData` can
|
|
97
|
+
* run. `store` and `notifyEmail` are deliberately left off — this plugin stores
|
|
98
|
+
* submissions as documents and sends notifications from its own consumer, so the
|
|
99
|
+
* definition is used purely as a validation contract.
|
|
100
|
+
*/
|
|
101
|
+
export function compileForm(doc) {
|
|
102
|
+
const fields = (doc.fields ?? []).map(toField).filter((f) => f !== null);
|
|
103
|
+
return {
|
|
104
|
+
id: doc.id,
|
|
105
|
+
title: doc.title ?? 'Form',
|
|
106
|
+
fields,
|
|
107
|
+
// Trim every string before validation. This is the transform half — the one
|
|
108
|
+
// thing validation can't do — so " " fails a required check instead of
|
|
109
|
+
// passing it.
|
|
110
|
+
transform: (data) => {
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [key, value] of Object.entries(data)) {
|
|
113
|
+
out[key] = typeof value === 'string' ? value.trim() : value;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../src/lib/compile.ts"],"names":[],"mappings":"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"}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Emitted once a submission has been stored.
|
|
4
|
+
*
|
|
5
|
+
* Carries identifiers only, in line with the rest of the event catalog: the log
|
|
6
|
+
* is not a content mirror, and a submission is the one payload most likely to
|
|
7
|
+
* hold a person's name, email and message. Keeping those out of the immutable
|
|
8
|
+
* log means a deletion request can be honoured by deleting the submission
|
|
9
|
+
* document — an append-only log that quietly held a second copy would make that
|
|
10
|
+
* impossible.
|
|
11
|
+
*
|
|
12
|
+
* The notification consumer reads the submission back to build the email.
|
|
13
|
+
*/
|
|
14
|
+
export declare const formSubmissionCreated: import("@aphexcms/cms-core").EventDefinition<"forms.submission.created", z.ZodObject<{
|
|
15
|
+
submissionId: z.ZodString;
|
|
16
|
+
formId: z.ZodString;
|
|
17
|
+
}, z.core.$strip>>;
|
|
18
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/lib/events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB;;;kBAMjC,CAAC"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineEvent } from '@aphexcms/cms-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Emitted once a submission has been stored.
|
|
5
|
+
*
|
|
6
|
+
* Carries identifiers only, in line with the rest of the event catalog: the log
|
|
7
|
+
* is not a content mirror, and a submission is the one payload most likely to
|
|
8
|
+
* hold a person's name, email and message. Keeping those out of the immutable
|
|
9
|
+
* log means a deletion request can be honoured by deleting the submission
|
|
10
|
+
* document — an append-only log that quietly held a second copy would make that
|
|
11
|
+
* impossible.
|
|
12
|
+
*
|
|
13
|
+
* The notification consumer reads the submission back to build the email.
|
|
14
|
+
*/
|
|
15
|
+
export const formSubmissionCreated = defineEvent('forms.submission.created', z.object({
|
|
16
|
+
submissionId: z.string(),
|
|
17
|
+
formId: z.string()
|
|
18
|
+
}));
|
|
19
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/lib/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,WAAW,CAC/C,0BAA0B,EAC1B,CAAC,CAAC,MAAM,CAAC;IACR,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CAClB,CAAC,CACF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SchemaAccess } from '@aphexcms/cms-core';
|
|
2
|
+
export interface FormsPluginOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The address notification emails are sent from. The app owns its sending
|
|
5
|
+
* identity, so this is passed in rather than guessed; falls back to
|
|
6
|
+
* `APHEX_EMAIL_FROM`. With neither set, submissions are still stored and a
|
|
7
|
+
* configured notification retries, then dead-letters visibly in Activity.
|
|
8
|
+
*/
|
|
9
|
+
from?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Override submission collection access. Defaults to owner/admin read and
|
|
12
|
+
* delete access, with direct create/update/publish operations blocked. Partial
|
|
13
|
+
* overrides are merged with those defaults, so adding a custom reviewer role
|
|
14
|
+
* does not accidentally reopen writes.
|
|
15
|
+
*/
|
|
16
|
+
submissionAccess?: SchemaAccess;
|
|
17
|
+
}
|
|
18
|
+
export declare function formsPlugin(options?: FormsPluginOptions): import("@aphexcms/cms-core").CMSPlugin;
|
|
19
|
+
export { defaultSubmissionAccess, formSchema, formSubmissionSchema, formFieldTypes } from './schema.js';
|
|
20
|
+
export { compileForm, isInputField, optionValue } from './compile.js';
|
|
21
|
+
export type { FormDocument, FormFieldValue, NotificationEmailValue } from './compile.js';
|
|
22
|
+
export { formSubmissionCreated } from './events.js';
|
|
23
|
+
export { renderTemplate, fieldLabels, toStoredValue } from './template.js';
|
|
24
|
+
export type { Answer } from './template.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAA0B,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQ/E,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,YAAY,CAAC;CAChC;AAOD,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,0CAyC3D;AAED,OAAO,EACN,uBAAuB,EACvB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACnE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACxE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party form-builder plugin — comparable to `@payloadcms/plugin-form-builder`.
|
|
3
|
+
*
|
|
4
|
+
* `formsPlugin()` contributes everything a form needs and nothing an app has to
|
|
5
|
+
* wire:
|
|
6
|
+
*
|
|
7
|
+
* - two collections, `form` and `formSubmission` (`aphex/schema`)
|
|
8
|
+
* - `POST /api/form-submissions`, public, with a honeypot and rate limiting
|
|
9
|
+
* (`aphex/server/route`)
|
|
10
|
+
* - a consumer that emails the form's notification list out of band
|
|
11
|
+
* (`aphex/event/consumer`)
|
|
12
|
+
*
|
|
13
|
+
* Add it to the plugins array and an editor can build a form; nothing goes in
|
|
14
|
+
* `aphex.config.ts` beyond that.
|
|
15
|
+
*
|
|
16
|
+
* ## What the app still owns
|
|
17
|
+
*
|
|
18
|
+
* Rendering. The plugin defines what a form *is* and what happens when one is
|
|
19
|
+
* submitted; how it looks belongs to the site, so the template ships a `FormBlock`
|
|
20
|
+
* component that reads a form document and posts to the endpoint. That split is
|
|
21
|
+
* deliberate — a plugin that shipped markup would have to ship a design with it.
|
|
22
|
+
*
|
|
23
|
+
* ## Layering
|
|
24
|
+
*
|
|
25
|
+
* cms-core already has `defineForm` — a form a *developer* writes in code. This
|
|
26
|
+
* plugin is the same idea with the authoring moved into the studio: a `form`
|
|
27
|
+
* document is compiled into a `FormDefinition` and validated by the identical
|
|
28
|
+
* engine (see `compile.ts`). One validator, two ways to author.
|
|
29
|
+
*/
|
|
30
|
+
import { definePlugin, effectiveOrganizationRole } from '@aphexcms/cms-core';
|
|
31
|
+
import { formSchema, formSubmissionSchema } from './schema.js';
|
|
32
|
+
import { handleSubmit } from './submit.js';
|
|
33
|
+
import { notifyOnSubmission } from './notify.js';
|
|
34
|
+
import { formSubmissionCreated } from './events.js';
|
|
35
|
+
const PLUGIN_ID = '@aphexcms/plugin-forms';
|
|
36
|
+
function strictSubmissionRule(rule) {
|
|
37
|
+
if (!Array.isArray(rule))
|
|
38
|
+
return rule;
|
|
39
|
+
return ({ auth }) => rule.includes(effectiveOrganizationRole(auth) ?? '');
|
|
40
|
+
}
|
|
41
|
+
export function formsPlugin(options = {}) {
|
|
42
|
+
const mergedAccess = { ...formSubmissionSchema.access, ...options.submissionAccess };
|
|
43
|
+
const submissionSchema = {
|
|
44
|
+
...formSubmissionSchema,
|
|
45
|
+
access: Object.fromEntries(Object.entries(mergedAccess).map(([operation, rule]) => [
|
|
46
|
+
operation,
|
|
47
|
+
strictSubmissionRule(rule)
|
|
48
|
+
]))
|
|
49
|
+
};
|
|
50
|
+
const parts = [
|
|
51
|
+
{
|
|
52
|
+
implements: 'aphex/schema',
|
|
53
|
+
schemas: [formSchema, submissionSchema]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
implements: 'aphex/server/route',
|
|
57
|
+
id: 'forms.submit',
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: '/form-submissions',
|
|
60
|
+
handler: handleSubmit,
|
|
61
|
+
/*
|
|
62
|
+
* The one endpoint here that must be open: a form is for people who
|
|
63
|
+
* don't have accounts. Opting out of the capability gate is deliberate
|
|
64
|
+
* and the handler does its own verification — it validates against the
|
|
65
|
+
* form's declared fields, drops undeclared keys, rate-limits per IP and
|
|
66
|
+
* carries a honeypot. See `submit.ts`.
|
|
67
|
+
*/
|
|
68
|
+
requiredCapabilities: 'public'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
implements: 'aphex/event/consumer',
|
|
72
|
+
id: 'forms.notify',
|
|
73
|
+
events: [formSubmissionCreated.type],
|
|
74
|
+
handler: notifyOnSubmission(options.from)
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
return definePlugin({ name: PLUGIN_ID, version: '0.1.0', parts });
|
|
78
|
+
}
|
|
79
|
+
export { defaultSubmissionAccess, formSchema, formSubmissionSchema, formFieldTypes } from './schema.js';
|
|
80
|
+
export { compileForm, isInputField, optionValue } from './compile.js';
|
|
81
|
+
export { formSubmissionCreated } from './events.js';
|
|
82
|
+
export { renderTemplate, fieldLabels, toStoredValue } from './template.js';
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,YAAY,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,MAAM,SAAS,GAAG,wBAAwB,CAAC;AAoB3C,SAAS,oBAAoB,CAAC,IAA4B;IACzD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,UAA8B,EAAE;IAC3D,MAAM,YAAY,GAAG,EAAE,GAAG,oBAAoB,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IACrF,MAAM,gBAAgB,GAAgC;QACrD,GAAG,oBAAoB;QACvB,MAAM,EAAE,MAAM,CAAC,WAAW,CACzB,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;YACvD,SAAS;YACT,oBAAoB,CAAC,IAAI,CAAC;SAC1B,CAAC,CACc;KACjB,CAAC;IAEF,MAAM,KAAK,GAAiB;QAC3B;YACC,UAAU,EAAE,cAAc;YAC1B,OAAO,EAAE,CAAC,UAAU,EAAE,gBAAgB,CAAC;SACvC;QACD;YACC,UAAU,EAAE,oBAAoB;YAChC,EAAE,EAAE,cAAc;YAClB,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,mBAAmB;YACzB,OAAO,EAAE,YAAY;YACrB;;;;;;eAMG;YACH,oBAAoB,EAAE,QAAQ;SAC9B;QACD;YACC,UAAU,EAAE,sBAAsB;YAClC,EAAE,EAAE,cAAc;YAClB,MAAM,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC;YACpC,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC;SACzC;KACD,CAAC;IAEF,OAAO,YAAY,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,OAAO,EACN,uBAAuB,EACvB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EventConsumerHandler } from '@aphexcms/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* Sends a form's notification emails, out of band.
|
|
4
|
+
*
|
|
5
|
+
* This runs as a delivery job, which is the entire reason a mail outage can't
|
|
6
|
+
* fail a visitor's submit: the submission is already stored, and a throw here
|
|
7
|
+
* retries with backoff and eventually dead-letters, visible in Activity. Sending
|
|
8
|
+
* inline in the request would have coupled "did my message go through" to
|
|
9
|
+
* whether an SMTP host answered.
|
|
10
|
+
*
|
|
11
|
+
* At-least-once delivery means this can run twice for one submission. A duplicate
|
|
12
|
+
* notification email is the acceptable failure — the alternative, tracking sent
|
|
13
|
+
* state to make it exactly-once, would have to be written somewhere that itself
|
|
14
|
+
* can fail between the send and the write.
|
|
15
|
+
*/
|
|
16
|
+
export declare function notifyOnSubmission(from?: string): EventConsumerHandler;
|
|
17
|
+
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/lib/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAK/D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAoEtE"}
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { formSubmissionCreated } from './events.js';
|
|
2
|
+
import { renderTemplate } from './template.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sends a form's notification emails, out of band.
|
|
5
|
+
*
|
|
6
|
+
* This runs as a delivery job, which is the entire reason a mail outage can't
|
|
7
|
+
* fail a visitor's submit: the submission is already stored, and a throw here
|
|
8
|
+
* retries with backoff and eventually dead-letters, visible in Activity. Sending
|
|
9
|
+
* inline in the request would have coupled "did my message go through" to
|
|
10
|
+
* whether an SMTP host answered.
|
|
11
|
+
*
|
|
12
|
+
* At-least-once delivery means this can run twice for one submission. A duplicate
|
|
13
|
+
* notification email is the acceptable failure — the alternative, tracking sent
|
|
14
|
+
* state to make it exactly-once, would have to be written somewhere that itself
|
|
15
|
+
* can fail between the send and the write.
|
|
16
|
+
*/
|
|
17
|
+
export function notifyOnSubmission(from) {
|
|
18
|
+
return async ({ event, databaseAdapter, emailAdapter, logger }) => {
|
|
19
|
+
/*
|
|
20
|
+
* The sending identity belongs to the app, not to this plugin — it's the same
|
|
21
|
+
* address the CMS's own password-reset and invitation mail goes out as. Passed
|
|
22
|
+
* in via `formsPlugin({ from })`, falling back to the variable the templates
|
|
23
|
+
* already use. Missing configuration throws when this submission has recipients,
|
|
24
|
+
* making the delivery retry and eventually surface in Activity instead of being
|
|
25
|
+
* acknowledged without sending.
|
|
26
|
+
*/
|
|
27
|
+
const { submissionId } = formSubmissionCreated.parse(event.payload);
|
|
28
|
+
const submissionRow = await databaseAdapter.findByDocIdAdvanced(event.organizationId, submissionId);
|
|
29
|
+
if (!submissionRow) {
|
|
30
|
+
// The document was deleted between the submit and this delivery. Nothing to
|
|
31
|
+
// send, and nothing that retrying would fix.
|
|
32
|
+
logger.warn(`[forms] submission ${submissionId} is gone; skipping notification`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// A submission is written as a draft and never published, so `draftData` is
|
|
36
|
+
// where both its answers and immutable notification snapshot live.
|
|
37
|
+
const submission = (submissionRow.draftData ?? {});
|
|
38
|
+
const emails = submission.notificationEmails ?? [];
|
|
39
|
+
if (emails.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
if (!emailAdapter)
|
|
42
|
+
throw new Error('[forms] notification email adapter is not configured');
|
|
43
|
+
const sender = from ?? process.env.APHEX_EMAIL_FROM;
|
|
44
|
+
if (!sender)
|
|
45
|
+
throw new Error('[forms] notification from-address is not configured');
|
|
46
|
+
const answers = (submission.submissionData ?? []).map((a) => ({
|
|
47
|
+
field: String(a.field ?? ''),
|
|
48
|
+
value: String(a.value ?? '')
|
|
49
|
+
}));
|
|
50
|
+
const labels = new Map((submission.notificationFieldLabels ?? []).map((entry) => [
|
|
51
|
+
String(entry.field ?? ''),
|
|
52
|
+
String(entry.label ?? entry.field ?? '')
|
|
53
|
+
]));
|
|
54
|
+
for (const email of emails) {
|
|
55
|
+
const to = (email.to ?? '')
|
|
56
|
+
.split(',')
|
|
57
|
+
.map((address) => address.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
if (to.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
const result = await emailAdapter.send({
|
|
62
|
+
from: sender,
|
|
63
|
+
to,
|
|
64
|
+
replyTo: email.replyTo,
|
|
65
|
+
subject: renderTemplate(email.subject ?? 'New form submission', answers, labels)
|
|
66
|
+
.replace(/[\r\n]+/g, ' ')
|
|
67
|
+
.slice(0, 998),
|
|
68
|
+
text: renderTemplate(email.message ?? '{{allFields}}', answers, labels)
|
|
69
|
+
});
|
|
70
|
+
if (result.error)
|
|
71
|
+
throw new Error(`[forms] notification email failed: ${result.error}`);
|
|
72
|
+
}
|
|
73
|
+
logger.info(`[forms] sent ${emails.length} notification(s) for submission ${submissionId}`);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../src/lib/notify.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SchemaAccess, SchemaType, TypeReference } from '@aphexcms/cms-core';
|
|
2
|
+
/** Every field kind an editor can add, in the order they appear in the "add" menu. */
|
|
3
|
+
export declare const formFieldTypes: TypeReference[];
|
|
4
|
+
/** Field kinds that actually collect an answer — everything except `formMessage`. */
|
|
5
|
+
export declare const INPUT_FIELD_TYPES: string[];
|
|
6
|
+
export declare const formSchema: SchemaType;
|
|
7
|
+
/**
|
|
8
|
+
* Submissions can contain personal data, so the general document capabilities
|
|
9
|
+
* are too broad. The submit route writes with system access; humans may only
|
|
10
|
+
* read or erase submissions when their organization role explicitly allows it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const defaultSubmissionAccess: SchemaAccess;
|
|
13
|
+
export declare const formSubmissionSchema: SchemaType;
|
|
14
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/lib/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA0PzF,sFAAsF;AACtF,eAAO,MAAM,cAAc,EAAE,aAAa,EAQzC,CAAC;AAEF,qFAAqF;AACrF,eAAO,MAAM,iBAAiB,UAEY,CAAC;AAE3C,eAAO,MAAM,UAAU,EAAE,UAwIxB,CAAC;AAsBF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,YAOrC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,UAqFlC,CAAC"}
|