@stacksjs/forms 0.71.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2023 Open Web Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @stacksjs/forms
2
+
3
+ Native form definitions, conditional fields, validation, submissions, and
4
+ notification helpers for Stacks applications.
5
+
6
+ ## Configuration
7
+
8
+ Forms are an optional framework feature. Enable the bundle with:
9
+
10
+ ```bash
11
+ ./buddy forms:install
12
+ ```
13
+
14
+ This enables `config/forms.ts` and the model-generated migrations for forms,
15
+ fields, and submissions. Disable it with `./buddy forms:uninstall`.
16
+
17
+ ## Runtime API
18
+
19
+ The package exposes typed helpers for:
20
+
21
+ - defining and validating form schemas
22
+ - evaluating conditional field visibility
23
+ - normalizing and storing submissions
24
+ - delivering submission notifications
25
+
26
+ Application-specific form models and actions remain under `app/`. Reusable
27
+ form behavior belongs in this package.
@@ -0,0 +1,12 @@
1
+ import type { FieldConditions, FormFieldDefinition } from './types';
2
+ /**
3
+ * Evaluate one field's visibility rules against the submitted values.
4
+ * No conditions = visible. The same function runs client-side for UX and
5
+ * server-side for correctness - the SERVER evaluation is the one that
6
+ * counts: a hidden required field is not required, and values submitted
7
+ * for hidden fields are discarded, so a tampered client can neither skip
8
+ * a required visible field nor smuggle values through a hidden one.
9
+ */
10
+ export declare function evaluateConditions(conditions: FieldConditions | null | undefined, values: Record<string, unknown>): boolean;
11
+ /** The fields visible for a given value set, in position order. */
12
+ export declare function visibleFields(fields: FormFieldDefinition[], values: Record<string, unknown>): FormFieldDefinition[];
@@ -0,0 +1,14 @@
1
+ import type { FormDefinition } from './types';
2
+ /**
3
+ * Load a form + its fields by uuid. `siteId` is REQUIRED and matched against
4
+ * the row (null site allowed for single-site apps passing null): a form uuid
5
+ * from one school must not render or accept submissions on another school's
6
+ * host.
7
+ */
8
+ export declare function loadFormByUuid(uuid: string, siteId: number | null): Promise<FormDefinition | null>;
9
+ /**
10
+ * The client-facing definition: everything a renderer needs, nothing an
11
+ * attacker wants (notify addresses, payment internals beyond what the UI
12
+ * must show).
13
+ */
14
+ export declare function publicDefinition(form: FormDefinition): Record<string, unknown>;
@@ -0,0 +1,22 @@
1
+ export type { SubmissionListRow, SubmitOptions, SubmitResult } from './submissions';
2
+ export type {
3
+ FieldChoice,
4
+ FieldConditions,
5
+ FieldOptions,
6
+ FormDefinition,
7
+ FormFieldDefinition,
8
+ FormFieldType,
9
+ FormSettings,
10
+ SubmissionErrors,
11
+ ValidateSubmissionResult,
12
+ } from './types';
13
+ export { evaluateConditions, visibleFields } from './conditions';
14
+ export { loadFormByUuid, publicDefinition } from './definition';
15
+ export { dispatchSubmissionNotifications } from './notifications';
16
+ export {
17
+ completeSubmissionPayment,
18
+ exportSubmissionsCsv,
19
+ fetchSubmissions,
20
+ submitForm,
21
+ } from './submissions';
22
+ export { computeAmountCents, validateSubmission } from './validate';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ export{t as visibleFields,a as validateSubmission,f as submitForm,n as publicDefinition,m as loadFormByUuid,d as fetchSubmissions,b as exportSubmissionsCsv,s as evaluateConditions,u as dispatchSubmissionNotifications,S as computeAmountCents,l as completeSubmissionPayment};
@@ -0,0 +1,9 @@
1
+ import type { FormDefinition } from './types';
2
+ import type { SubmitResult } from './submissions';
3
+ /**
4
+ * Post-submission notifications: staff notice to `settings.notifyEmails`,
5
+ * confirmation to the submitter when the form captured an email. Called by
6
+ * the route AFTER the write succeeds; every failure is logged, none block
7
+ * or undo the submission - the person's answers are already safe.
8
+ */
9
+ export declare function dispatchSubmissionNotifications(form: FormDefinition, result: Extract<SubmitResult, { ok: true }>, submission: { email: string | null, name: string | null, values: Record<string, unknown> }): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import type { FormDefinition, SubmissionErrors } from './types';
2
+ /**
3
+ * Accept a public submission. The caller (route/action) has already resolved
4
+ * the form for the request's site via `loadFormByUuid` - this validates,
5
+ * spam-guards, stores, and reports what should happen next (payment or
6
+ * confirmation). Notifications are the caller's follow-up so transports
7
+ * never block the write.
8
+ */
9
+ export declare function submitForm(form: FormDefinition, payload: Record<string, unknown>, options?: SubmitOptions): Promise<SubmitResult>;
10
+ /** Flip a paid submission to complete. Called from the payment webhook. */
11
+ export declare function completeSubmissionPayment(submissionUuid: string, paymentIntentId: string): Promise<boolean>;
12
+ /** Admin list, newest first. `formId` scoping is the caller's job to have authorized. */
13
+ export declare function fetchSubmissions(formId: number, options?: { limit?: number, offset?: number }): Promise<SubmissionListRow[]>;
14
+ /** The full submission set as CSV: field columns in position order + the typed columns. */
15
+ export declare function exportSubmissionsCsv(form: FormDefinition): Promise<string>;
16
+ export declare interface SubmitOptions {
17
+ ip?: string
18
+ renderedAtMs?: number
19
+ honeypot?: string
20
+ }
21
+ export declare interface SubmissionListRow {
22
+ id: number
23
+ uuid: string
24
+ values: Record<string, unknown>
25
+ email: string | null
26
+ name: string | null
27
+ status: string
28
+ amountCents: number | null
29
+ submittedAt: string | null
30
+ }
31
+ export type SubmitResult = | { ok: true, submissionId: number, submissionUuid: string, status: 'complete' | 'pending_payment', amountCents: number | null, confirmation: string | null, redirect: string | null }
32
+ | { ok: false, status: 422, errors: SubmissionErrors }
33
+ | { ok: false, status: 404 | 409 | 429, message: string }
@@ -0,0 +1,64 @@
1
+ export declare interface FieldChoice {
2
+ label: string
3
+ value: string
4
+ }
5
+ export declare interface FieldOptions {
6
+ placeholder?: string
7
+ choices?: FieldChoice[]
8
+ min?: number
9
+ max?: number
10
+ accept?: string[]
11
+ maxSizeMb?: number
12
+ amountCents?: number
13
+ }
14
+ export declare interface FieldConditions {
15
+ action: 'show' | 'hide'
16
+ logic: 'all' | 'any'
17
+ rules: {
18
+ field: string
19
+ op: 'eq' | 'neq' | 'contains' | 'gt' | 'lt' | 'empty' | 'not_empty'
20
+ value?: string | number
21
+ }[]
22
+ }
23
+ /** The runtime shape of a form field, parsed from its row. */
24
+ export declare interface FormFieldDefinition {
25
+ name: string
26
+ label: string
27
+ type: FormFieldType
28
+ required: boolean
29
+ position: number
30
+ width: 'full' | 'half'
31
+ options: FieldOptions
32
+ conditions: FieldConditions | null
33
+ }
34
+ export declare interface FormSettings {
35
+ submitLabel?: string
36
+ confirmation?: { type: 'message' | 'redirect', value: string }
37
+ notifyEmails?: string[]
38
+ emailField?: string
39
+ nameField?: string
40
+ payment?: {
41
+ mode: 'fixed' | 'user_amount' | 'field_sum'
42
+ amountCents?: number
43
+ currency?: string
44
+ amountField?: string
45
+ minAmountCents?: number
46
+ }
47
+ }
48
+ export declare interface FormDefinition {
49
+ id: number
50
+ uuid: string
51
+ siteId: number | null
52
+ name: string
53
+ handle: string
54
+ status: 'draft' | 'active' | 'closed'
55
+ settings: FormSettings
56
+ fields: FormFieldDefinition[]
57
+ }
58
+ export declare interface SubmissionErrors {
59
+ [fieldName: string]: string
60
+ }
61
+ export type FormFieldType = | 'text' | 'textarea' | 'email' | 'phone' | 'select' | 'checkbox'
62
+ | 'radio' | 'date' | 'file' | 'currency' | 'section_break';
63
+ export type ValidateSubmissionResult = | { ok: true, values: Record<string, unknown>, email: string | null, name: string | null, amountCents: number | null }
64
+ | { ok: false, errors: SubmissionErrors }
@@ -0,0 +1,17 @@
1
+ import type { FormDefinition, FormFieldDefinition, ValidateSubmissionResult } from './types';
2
+ /**
3
+ * Server-computed payment amount. NEVER trusts a client total:
4
+ * `fixed` reads the form's setting, `field_sum` adds the currency fields'
5
+ * validated values, `user_amount` takes the named field but enforces the
6
+ * configured floor.
7
+ */
8
+ export declare function computeAmountCents(form: FormDefinition, fields: FormFieldDefinition[], values: Record<string, unknown>): number | null;
9
+ /**
10
+ * Validate an untrusted submission against a form definition.
11
+ *
12
+ * Visibility first: required-ness and value acceptance apply only to fields
13
+ * visible under the submitted values, and values for hidden or unknown
14
+ * fields are DISCARDED - the stored document contains exactly what the
15
+ * person could see.
16
+ */
17
+ export declare function validateSubmission(form: FormDefinition, payload: Record<string, unknown>): ValidateSubmissionResult;
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@stacksjs/forms",
3
+ "type": "module",
4
+ "version": "0.71.1",
5
+ "description": "The Stacks form-builder functionality.",
6
+ "author": "Chris Breuer",
7
+ "contributors": [
8
+ "Chris Breuer <chris@stacksjs.com>"
9
+ ],
10
+ "license": "MIT",
11
+ "funding": "https://github.com/sponsors/chrisbbreuer",
12
+ "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/forms#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/stacksjs/stacks.git",
16
+ "directory": "./storage/framework/core/forms"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/stacksjs/stacks/issues"
20
+ },
21
+ "keywords": [
22
+ "forms",
23
+ "form-builder",
24
+ "submissions",
25
+ "stacks"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "bun": "./dist/index.js",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./*": {
35
+ "types": "./dist/*.d.ts",
36
+ "bun": "./dist/*.js",
37
+ "import": "./dist/*.js",
38
+ "default": "./dist/*.js"
39
+ },
40
+ "./*.js": {
41
+ "types": "./dist/*.d.ts",
42
+ "bun": "./dist/*.js",
43
+ "import": "./dist/*.js",
44
+ "default": "./dist/*.js"
45
+ }
46
+ },
47
+ "module": "dist/index.js",
48
+ "types": "dist/index.d.ts",
49
+ "files": [
50
+ "README.md",
51
+ "dist"
52
+ ],
53
+ "scripts": {
54
+ "build": "bun build.ts",
55
+ "typecheck": "bun tsc --noEmit",
56
+ "prepublishOnly": "bun run build"
57
+ },
58
+ "devDependencies": {
59
+ "@stacksjs/config": "0.71.1",
60
+ "@stacksjs/database": "0.71.1",
61
+ "@stacksjs/notifications": "0.71.1",
62
+ "@stacksjs/storage": "0.71.1",
63
+ "@stacksjs/validation": "0.71.1",
64
+ "better-dx": "^0.2.23"
65
+ },
66
+ "sideEffects": false
67
+ }