@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 +21 -0
- package/README.md +27 -0
- package/dist/conditions.d.ts +12 -0
- package/dist/definition.d.ts +14 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +2 -0
- package/dist/notifications.d.ts +9 -0
- package/dist/submissions.d.ts +33 -0
- package/dist/types.d.ts +64 -0
- package/dist/validate.d.ts +17 -0
- package/package.json +67 -0
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>;
|
package/dist/index.d.ts
ADDED
|
@@ -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 }
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|