@taprootio/site-authoring 0.12.5 → 0.12.7
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 +13 -3
- package/package.json +2 -2
- package/src/capabilities.js +1 -1
- package/src/cli.js +50 -2
- package/src/constants.js +3 -0
- package/src/content/components.js +21 -0
- package/src/content/free-form-section-registry.json +6 -1
- package/src/delivery-check.js +2 -11
- package/src/field-schema.json +217 -0
- package/src/field-validation.js +715 -0
- package/src/fixture-contract.js +1 -1
- package/src/fixture-init.js +15 -0
- package/src/forms-api.js +192 -0
- package/src/forms-contract.js +274 -0
- package/src/forms-workspace.js +100 -0
- package/src/reference-help.js +154 -6
- package/src/settings-catalog.js +3 -0
- package/src/verbs/delivery-check.js +0 -7
- package/src/verbs/forms-pull.js +67 -0
- package/src/verbs/forms-push.js +227 -0
- package/src/verbs/forms-validate.js +36 -0
- package/src/verbs/index.js +11 -0
- package/src/verbs/pull.js +8 -0
- package/src/verbs/validate.js +57 -0
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ Which verbs apply depends on the site:
|
|
|
188
188
|
`pull` snapshots its four settings documents, `theme push`, `footer push`,
|
|
189
189
|
and `media upload` author them, and `deploy` stages and promotes them.
|
|
190
190
|
Its pages, navigation, and redirects come from the Docs artifact, so
|
|
191
|
-
`pages push`, `nav push`, the `redirects` verbs, `approve`, and the
|
|
191
|
+
`pages push`, `nav push`, the `redirects` and `forms` verbs, `approve`, and the
|
|
192
192
|
`preview` verbs refuse with `surface.presentation_only` before any request.
|
|
193
193
|
- A **prebuilt Docs** site (`none`) serves its artifact unchanged and takes no
|
|
194
194
|
verb; each refuses with `surface.none`.
|
|
@@ -373,6 +373,16 @@ validates the file offline and replaces the whole map; `help redirects`
|
|
|
373
373
|
states the entry shape (`path`, `kind` redirect or gone, `target`, `status`
|
|
374
374
|
defaulting to 301), the normalization rule, and every refusal.
|
|
375
375
|
|
|
376
|
+
`forms pull` writes each live form to `forms/<key>.json` and records the
|
|
377
|
+
version it read; `forms push` validates every file, refuses a form the site
|
|
378
|
+
changed since (`forms.concurrent_modification`) and creates or updates the
|
|
379
|
+
rest, appending a version when the definition changed. `forms validate` checks
|
|
380
|
+
the files offline against the shared field schema with no credential. Both
|
|
381
|
+
verbs write one canonical key order, so a pulled file pushed back is unchanged.
|
|
382
|
+
Submissions are never pulled. `help forms` states the file shape and every
|
|
383
|
+
refusal, and placing a form on a page is the free-form `form`
|
|
384
|
+
component.
|
|
385
|
+
|
|
376
386
|
`preview page <page-path-or-id>` renders one persisted draft behind the
|
|
377
387
|
staging gate and mints a browser handoff URL, reported only in the final JSON
|
|
378
388
|
result. **Each handoff is single-use.** Opening the URL consumes it; a reused,
|
|
@@ -537,8 +547,8 @@ completed deployment for the target, HTTP delivery of the authored routes and
|
|
|
537
547
|
the favicon, images, module preloads and site bundle they declare (status and
|
|
538
548
|
content type, no redirects followed), internal link targets, accidental
|
|
539
549
|
local-only references, and the pinned runtime — the version the page names,
|
|
540
|
-
its entry module and the capability modules the page declares
|
|
541
|
-
|
|
550
|
+
its entry module and the capability modules the page declares (a page that
|
|
551
|
+
pins no runtime version and entry fails the runtime check). The
|
|
542
552
|
browser dimension (a fresh and a returning load
|
|
543
553
|
compared against the page's pinned entry, every document request in the browser,
|
|
544
554
|
popups included, held to the site origin before it is sent, and cache reuse
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@taprootio/espalier": "
|
|
31
|
+
"@taprootio/espalier": "5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node --test",
|
package/src/capabilities.js
CHANGED
|
@@ -23,7 +23,7 @@ export const SITE_AUTHORING_CAPABILITIES = Object.freeze([
|
|
|
23
23
|
|
|
24
24
|
/** What each capability lets an exchanged credential do, for help and `whoami`. */
|
|
25
25
|
export const SITE_AUTHORING_CAPABILITY_SUMMARIES = Object.freeze({
|
|
26
|
-
"delegation.content": "write pages and
|
|
26
|
+
"delegation.content": "write pages, media, and form definitions",
|
|
27
27
|
"delegation.design": "set the theme, navigation, and footer",
|
|
28
28
|
"delegation.deployments": "stage and deploy",
|
|
29
29
|
});
|
package/src/cli.js
CHANGED
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
VERB_STAGING_REVIEW,
|
|
21
21
|
VERB_ENV,
|
|
22
22
|
VERB_FOOTER_PUSH,
|
|
23
|
+
VERB_FORMS_PULL,
|
|
24
|
+
VERB_FORMS_PUSH,
|
|
25
|
+
VERB_FORMS_VALIDATE,
|
|
23
26
|
VERB_HELP,
|
|
24
27
|
VERB_LOGIN,
|
|
25
28
|
VERB_LOGOUT,
|
|
@@ -71,14 +74,14 @@ import { VERB_HANDLERS } from "./verbs/index.js";
|
|
|
71
74
|
/**
|
|
72
75
|
* The verbs the local version gate applies to (TR00703).
|
|
73
76
|
*
|
|
74
|
-
* Exactly the
|
|
77
|
+
* Exactly the ones that make no request. Every other verb reaches Taproot,
|
|
75
78
|
* whose own refusal is authoritative and better informed than this recording,
|
|
76
79
|
* so gating them here would only duplicate an answer — and gating `login`,
|
|
77
80
|
* `logout`, or `env` would take away the commands an operator needs while they
|
|
78
81
|
* are outdated. `--help` and `--version` are outside it for the same reason:
|
|
79
82
|
* they are how someone finds out what they have.
|
|
80
83
|
*/
|
|
81
|
-
const VERSION_GATED_OFFLINE_VERBS = Object.freeze([VERB_HELP, VERB_VALIDATE, VERB_WHOAMI]);
|
|
84
|
+
const VERSION_GATED_OFFLINE_VERBS = Object.freeze([VERB_HELP, VERB_VALIDATE, VERB_FORMS_VALIDATE, VERB_WHOAMI]);
|
|
82
85
|
|
|
83
86
|
// One entry per verb, in help order. `tokens` is the exact leading positional
|
|
84
87
|
// sequence; matching prefers the longest sequence, so a two-token family can
|
|
@@ -281,6 +284,47 @@ const VERBS = Object.freeze([
|
|
|
281
284
|
+ "goes live with that deploy: staging first, production when promoted. "
|
|
282
285
|
+ "See 'taproot-site help redirects'.",
|
|
283
286
|
},
|
|
287
|
+
{
|
|
288
|
+
name: VERB_FORMS_PULL,
|
|
289
|
+
surface: SURFACE_STANDARD,
|
|
290
|
+
// Forms are authored with the pages that place them, so they ride the
|
|
291
|
+
// content capability; the site's forms permissions decide what it may do.
|
|
292
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
293
|
+
tokens: ["forms", "pull"],
|
|
294
|
+
summary: "Write each live form to forms/<key>.json and record the versions read.",
|
|
295
|
+
note: "Needs a pulled workspace ('pull' first). It replaces a form's file with the site's copy and records the "
|
|
296
|
+
+ "version, which 'forms push' uses to refuse a change made on the site since. An archived form is listed "
|
|
297
|
+
+ "and not written. Submissions are never pulled: they are visitors' personal data and stay on the site. "
|
|
298
|
+
+ "See 'taproot-site help forms'.",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: VERB_FORMS_PUSH,
|
|
302
|
+
surface: SURFACE_STANDARD,
|
|
303
|
+
capabilities: [CAPABILITY_CONTENT],
|
|
304
|
+
tokens: ["forms", "push"],
|
|
305
|
+
positionals: "formKeys",
|
|
306
|
+
summary: "Validate forms/<key>.json and create or update each form on the site.",
|
|
307
|
+
note: "Every file is validated and compared with the site before anything is written. A form whose site "
|
|
308
|
+
+ "version moved since the last pull is refused (forms.concurrent_modification), and a form the site has "
|
|
309
|
+
+ "that this workspace never pulled is refused (forms.pull_required). Editing a definition appends a version; "
|
|
310
|
+
+ "a page keeps the version it was published with until the next deploy. An after_submit page_path is looked "
|
|
311
|
+
+ "up on the site, and a path that names no page is refused (forms.page_not_found), as is a generated, 404, legal, profile or integration page (forms.page_not_allowed). Name form keys to narrow "
|
|
312
|
+
+ "the push. See 'taproot-site help forms'.",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: VERB_FORMS_VALIDATE,
|
|
316
|
+
tokens: ["forms", "validate"],
|
|
317
|
+
offline: true,
|
|
318
|
+
// Offline, but it reads taproot-site.json to find the workspace.
|
|
319
|
+
readsLocalState: true,
|
|
320
|
+
positionals: "formKeys",
|
|
321
|
+
summary: "Validate the workspace's form files offline, against the shared field schema.",
|
|
322
|
+
note: "Reads forms/<key>.json and checks the file contract and every field definition with the same validator "
|
|
323
|
+
+ "'forms push' and the published form use. Name form keys to narrow it; with none, every file under forms/ "
|
|
324
|
+
+ "is checked. It cannot prove that the site accepts the key, that an after_submit page_path names a page, or "
|
|
325
|
+
+ "that the caller may write forms. "
|
|
326
|
+
+ "See 'taproot-site help forms'.",
|
|
327
|
+
},
|
|
284
328
|
{
|
|
285
329
|
name: VERB_THEME_PUSH,
|
|
286
330
|
surface: SURFACE_DOCS_PRESENTATION,
|
|
@@ -556,6 +600,8 @@ function verbHelp(verb) {
|
|
|
556
600
|
? " [production | local | <url>]"
|
|
557
601
|
: verb.positionals === "siteSelector"
|
|
558
602
|
? " <site-name-or-id>"
|
|
603
|
+
: verb.positionals === "formKeys"
|
|
604
|
+
? " [form-key...]"
|
|
559
605
|
: verb.positionals
|
|
560
606
|
? ` [${verb.positionals === "paths" ? "path" : "page-path"}...]`
|
|
561
607
|
: "";
|
|
@@ -706,6 +752,7 @@ function parseReferenceArguments(arguments_) {
|
|
|
706
752
|
|| topic === "footer"
|
|
707
753
|
|| topic === "fixture"
|
|
708
754
|
|| topic === "redirects"
|
|
755
|
+
|| topic === "forms"
|
|
709
756
|
|| topic === "walkthrough"
|
|
710
757
|
|| topic === "delivery"
|
|
711
758
|
|| topic === "import"
|
|
@@ -773,6 +820,7 @@ function referenceResult(parsed) {
|
|
|
773
820
|
}
|
|
774
821
|
case "nav":
|
|
775
822
|
case "redirects":
|
|
823
|
+
case "forms":
|
|
776
824
|
case "media":
|
|
777
825
|
case "preview":
|
|
778
826
|
case "fixture":
|
package/src/constants.js
CHANGED
|
@@ -87,6 +87,9 @@ export const VERB_NAV_PUSH = "nav push";
|
|
|
87
87
|
export const VERB_REDIRECTS_CHECK = "redirects check";
|
|
88
88
|
export const VERB_REDIRECTS_PULL = "redirects pull";
|
|
89
89
|
export const VERB_REDIRECTS_PUSH = "redirects push";
|
|
90
|
+
export const VERB_FORMS_PULL = "forms pull";
|
|
91
|
+
export const VERB_FORMS_PUSH = "forms push";
|
|
92
|
+
export const VERB_FORMS_VALIDATE = "forms validate";
|
|
90
93
|
export const VERB_THEME_PUSH = "theme push";
|
|
91
94
|
export const VERB_FOOTER_PUSH = "footer push";
|
|
92
95
|
export const VERB_MEDIA_UPLOAD = "media upload";
|
|
@@ -286,6 +286,15 @@ const SPACER = Object.freeze({
|
|
|
286
286
|
dividerWidth: num({ integer: true, minimum: 1 }),
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* A form defined under Site → Forms (or with `forms push`), placed by key. The
|
|
291
|
+
* key is checked against the site's forms when the page is published, not here:
|
|
292
|
+
* this table only knows the shape.
|
|
293
|
+
*/
|
|
294
|
+
const FORM = Object.freeze({
|
|
295
|
+
formKey: str(),
|
|
296
|
+
});
|
|
297
|
+
|
|
289
298
|
function deepFreeze(value) {
|
|
290
299
|
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
291
300
|
for (const child of Object.values(value)) deepFreeze(child);
|
|
@@ -555,6 +564,18 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
555
564
|
imageMotion: "parallax-subtle",
|
|
556
565
|
},
|
|
557
566
|
),
|
|
567
|
+
"form": component(
|
|
568
|
+
FORM,
|
|
569
|
+
"Form",
|
|
570
|
+
"A form from Site → Forms that visitors fill in and submit.",
|
|
571
|
+
{ formKey: "" },
|
|
572
|
+
[
|
|
573
|
+
"Set formKey to the key of an existing form ('forms push' creates it); an unknown or archived key fails the publish.",
|
|
574
|
+
"The button text and what happens after a response is sent belong to the form, not the page: set submit_label and after_submit in the form's file (see 'help forms').",
|
|
575
|
+
"The form's field labels are its accessible names; write each so it makes sense out of context.",
|
|
576
|
+
],
|
|
577
|
+
{ formKey: "contact" },
|
|
578
|
+
),
|
|
558
579
|
});
|
|
559
580
|
|
|
560
581
|
export const COMPONENT_TYPES = Object.freeze(Object.keys(COMPONENT_DEFINITIONS));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 9,
|
|
3
3
|
"section": {
|
|
4
4
|
"nodeType": "section",
|
|
5
5
|
"label": "Section",
|
|
@@ -554,6 +554,11 @@
|
|
|
554
554
|
"label": "Image Banner",
|
|
555
555
|
"measure": "wide",
|
|
556
556
|
"rootPlacement": "root-band"
|
|
557
|
+
},
|
|
558
|
+
"form": {
|
|
559
|
+
"label": "Form",
|
|
560
|
+
"measure": "wide",
|
|
561
|
+
"rootPlacement": "flow"
|
|
557
562
|
}
|
|
558
563
|
},
|
|
559
564
|
"integrationPlacement": {
|
package/src/delivery-check.js
CHANGED
|
@@ -205,8 +205,6 @@ export function parseBootstrap(html) {
|
|
|
205
205
|
return {
|
|
206
206
|
version: text(parsed.version).slice(0, 64),
|
|
207
207
|
entry: text(parsed.entry),
|
|
208
|
-
// A release published before runtime pinning named a mutable pointer instead.
|
|
209
|
-
legacyPointer: typeof parsed.runtimeManifestUrl === "string" && parsed.runtimeManifestUrl !== "",
|
|
210
208
|
siteBundleUrl: text(parsed.siteBundleUrl),
|
|
211
209
|
runtimeCapabilities,
|
|
212
210
|
capabilities,
|
|
@@ -506,9 +504,6 @@ async function observe(context, origin, sampledRoutes) {
|
|
|
506
504
|
}
|
|
507
505
|
}
|
|
508
506
|
runtime.pinned = runtime.version !== "" && Boolean(bootstrap?.entry);
|
|
509
|
-
// Such a page still loads in a browser, but this check no longer reads the
|
|
510
|
-
// pointer, so its runtime is reported unverified rather than as passed or failed.
|
|
511
|
-
runtime.legacy = !runtime.pinned && Boolean(bootstrap?.legacyPointer);
|
|
512
507
|
const entryUrl = runtime.pinned ? admit(bootstrap.entry) : undefined;
|
|
513
508
|
if (entryUrl) {
|
|
514
509
|
const { row } = await probe(context, entryUrl.href, "runtime-entry");
|
|
@@ -570,10 +565,7 @@ async function observe(context, origin, sampledRoutes) {
|
|
|
570
565
|
...assetResults.filter((row) => !row.ok).map((row) => `${row.kind} ${row.url}: ${row.failure}`),
|
|
571
566
|
...linkResults.filter((row) => !row.ok).map((row) => `link ${row.path}: ${row.failure}`),
|
|
572
567
|
...(runtime.declared ? [] : ["runtime: the home page declares no runtime bootstrap"]),
|
|
573
|
-
...(runtime.declared && runtime.
|
|
574
|
-
? ["runtime: the page names a pre-pinning runtime pointer, which is not verified here; republish the site so it pins a runtime version"]
|
|
575
|
-
: []),
|
|
576
|
-
...(runtime.declared && !runtime.pinned && !runtime.legacy
|
|
568
|
+
...(runtime.declared && !runtime.pinned
|
|
577
569
|
? ["runtime: the page does not pin a runtime version and entry"]
|
|
578
570
|
: []),
|
|
579
571
|
...(runtime.declared && runtime.pinned && !runtime.entry.ok
|
|
@@ -677,7 +669,7 @@ export async function checkDelivery({
|
|
|
677
669
|
|
|
678
670
|
const verdict = observed.failuresTotal === 0
|
|
679
671
|
? "delivered"
|
|
680
|
-
: observed.routeRows.every((row) => row.ok) &&
|
|
672
|
+
: observed.routeRows.every((row) => row.ok) && observed.runtime.compatible
|
|
681
673
|
? "degraded"
|
|
682
674
|
: "failed";
|
|
683
675
|
const report = {
|
|
@@ -757,7 +749,6 @@ function withinOutputBudget(report, failuresTotal) {
|
|
|
757
749
|
declared: runtime.declared,
|
|
758
750
|
version: runtime.version,
|
|
759
751
|
pinned: runtime.pinned,
|
|
760
|
-
legacy: runtime.legacy,
|
|
761
752
|
capabilities: runtime.capabilities.length,
|
|
762
753
|
capabilitiesResolvable: runtime.capabilitiesResolvable,
|
|
763
754
|
compatible: runtime.compatible,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://taproot.io/schemas/field-definitions.json",
|
|
4
|
+
"title": "Taproot field definitions",
|
|
5
|
+
"description": "A flat, closed list of typed fields. shared/field-validation.ts and the API validator interpret it; shared/field-validation-corpus.json is the evidence they agree. Limits: 30 fields, 16 KB of submitted content. JSON Schema maxLength counts code points while the validators count UTF-16 code units, so the validators are authoritative and stricter for astral characters.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["fields"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"fields": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"minItems": 1,
|
|
13
|
+
"maxItems": 30,
|
|
14
|
+
"items": { "$ref": "#/$defs/field" }
|
|
15
|
+
},
|
|
16
|
+
"contact_field": {
|
|
17
|
+
"description": "The id of the email field whose value the API copies to subject_email. It must name an email field. Omit it when the form has one email field (that field is the contact); it is required with two or more.",
|
|
18
|
+
"$ref": "#/$defs/id"
|
|
19
|
+
},
|
|
20
|
+
"submit_label": {
|
|
21
|
+
"description": "The text on the submit button. Omit it for Taproot's default. Must contain more than whitespace; the validator also rejects a blank label, which a schema cannot express.",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"maxLength": 40
|
|
25
|
+
},
|
|
26
|
+
"after_submit": {
|
|
27
|
+
"description": "What a visitor sees once a response is sent. Omit it to show Taproot's default message. A page is named by its resource id and must belong to the same site; the release freezes the path the page has when it is built.",
|
|
28
|
+
"oneOf": [
|
|
29
|
+
{
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["show"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"show": { "const": "message" },
|
|
35
|
+
"message": {
|
|
36
|
+
"description": "Omit it for Taproot's default. Must contain more than whitespace; the validator also rejects a blank message, which a schema cannot express.",
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"maxLength": 500
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["show", "page_resource_id"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"show": { "const": "page" },
|
|
49
|
+
"page_resource_id": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"$defs": {
|
|
56
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,39}$" },
|
|
57
|
+
"label": {
|
|
58
|
+
"description": "Must contain more than whitespace; the validator also rejects a blank label, which a schema cannot express.",
|
|
59
|
+
"type": "string",
|
|
60
|
+
"minLength": 1,
|
|
61
|
+
"maxLength": 200
|
|
62
|
+
},
|
|
63
|
+
"consentLabel": {
|
|
64
|
+
"description": "A consent field's label is the agreement the visitor accepts, so it is longer than any other label. Must contain more than whitespace; the validator also rejects a blank label.",
|
|
65
|
+
"type": "string",
|
|
66
|
+
"minLength": 1,
|
|
67
|
+
"maxLength": 1000
|
|
68
|
+
},
|
|
69
|
+
"hint": {
|
|
70
|
+
"description": "Optional helper text drawn under the field. Must contain more than whitespace; the validator rejects a blank hint, which a schema cannot express.",
|
|
71
|
+
"type": "string",
|
|
72
|
+
"minLength": 1,
|
|
73
|
+
"maxLength": 200
|
|
74
|
+
},
|
|
75
|
+
"required": { "type": "boolean" },
|
|
76
|
+
"shortLength": { "type": "integer", "minimum": 0, "maximum": 500 },
|
|
77
|
+
"shortMaxLength": { "type": "integer", "minimum": 1, "maximum": 500 },
|
|
78
|
+
"longLength": { "type": "integer", "minimum": 0, "maximum": 5000 },
|
|
79
|
+
"longMaxLength": { "type": "integer", "minimum": 1, "maximum": 5000 },
|
|
80
|
+
"choices": {
|
|
81
|
+
"description": "Each choice must contain more than whitespace; the validator rejects blank choices, which a schema cannot express.",
|
|
82
|
+
"type": "array",
|
|
83
|
+
"minItems": 1,
|
|
84
|
+
"maxItems": 50,
|
|
85
|
+
"uniqueItems": true,
|
|
86
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 100 }
|
|
87
|
+
},
|
|
88
|
+
"isoDate": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
|
|
89
|
+
"relativeDateBound": {
|
|
90
|
+
"description": "A date measured from the site's calendar date, resolved when the page loads and again when a submission arrives. offset_days is bounded to a year either side (366 covers a leap year); further out is a fixed date. A fixed date is the other form of a date bound.",
|
|
91
|
+
"type": "object",
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"required": ["from"],
|
|
94
|
+
"properties": {
|
|
95
|
+
"from": {
|
|
96
|
+
"enum": ["today", "start_of_month", "end_of_month", "start_of_next_month", "end_of_next_month"]
|
|
97
|
+
},
|
|
98
|
+
"offset_days": { "type": "integer", "minimum": -366, "maximum": 366 }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"dateBound": {
|
|
102
|
+
"oneOf": [{ "$ref": "#/$defs/isoDate" }, { "$ref": "#/$defs/relativeDateBound" }]
|
|
103
|
+
},
|
|
104
|
+
"field": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"required": ["id", "type", "label"],
|
|
107
|
+
"oneOf": [
|
|
108
|
+
{
|
|
109
|
+
"properties": {
|
|
110
|
+
"id": { "$ref": "#/$defs/id" },
|
|
111
|
+
"type": { "const": "text" },
|
|
112
|
+
"label": { "$ref": "#/$defs/label" },
|
|
113
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
114
|
+
"required": { "$ref": "#/$defs/required" },
|
|
115
|
+
"min_length": { "$ref": "#/$defs/shortLength" },
|
|
116
|
+
"max_length": { "$ref": "#/$defs/shortMaxLength" },
|
|
117
|
+
"format": { "enum": ["digits", "alphanumeric"] }
|
|
118
|
+
},
|
|
119
|
+
"additionalProperties": false
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"properties": {
|
|
123
|
+
"id": { "$ref": "#/$defs/id" },
|
|
124
|
+
"type": { "const": "long_text" },
|
|
125
|
+
"label": { "$ref": "#/$defs/label" },
|
|
126
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
127
|
+
"required": { "$ref": "#/$defs/required" },
|
|
128
|
+
"min_length": { "$ref": "#/$defs/longLength" },
|
|
129
|
+
"max_length": { "$ref": "#/$defs/longMaxLength" }
|
|
130
|
+
},
|
|
131
|
+
"additionalProperties": false
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"properties": {
|
|
135
|
+
"id": { "$ref": "#/$defs/id" },
|
|
136
|
+
"type": { "enum": ["email", "tel", "url"] },
|
|
137
|
+
"label": { "$ref": "#/$defs/label" },
|
|
138
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
139
|
+
"required": { "$ref": "#/$defs/required" },
|
|
140
|
+
"max_length": { "$ref": "#/$defs/shortMaxLength" }
|
|
141
|
+
},
|
|
142
|
+
"additionalProperties": false
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"properties": {
|
|
146
|
+
"id": { "$ref": "#/$defs/id" },
|
|
147
|
+
"type": { "const": "number" },
|
|
148
|
+
"label": { "$ref": "#/$defs/label" },
|
|
149
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
150
|
+
"required": { "$ref": "#/$defs/required" },
|
|
151
|
+
"min": { "type": "number" },
|
|
152
|
+
"max": { "type": "number" },
|
|
153
|
+
"format": { "enum": ["integer"] }
|
|
154
|
+
},
|
|
155
|
+
"additionalProperties": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"properties": {
|
|
159
|
+
"id": { "$ref": "#/$defs/id" },
|
|
160
|
+
"type": { "const": "date" },
|
|
161
|
+
"label": { "$ref": "#/$defs/label" },
|
|
162
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
163
|
+
"required": { "$ref": "#/$defs/required" },
|
|
164
|
+
"min": { "$ref": "#/$defs/dateBound" },
|
|
165
|
+
"max": { "$ref": "#/$defs/dateBound" }
|
|
166
|
+
},
|
|
167
|
+
"additionalProperties": false
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"required": ["choices"],
|
|
171
|
+
"properties": {
|
|
172
|
+
"id": { "$ref": "#/$defs/id" },
|
|
173
|
+
"type": { "const": "single_choice" },
|
|
174
|
+
"label": { "$ref": "#/$defs/label" },
|
|
175
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
176
|
+
"required": { "$ref": "#/$defs/required" },
|
|
177
|
+
"choices": { "$ref": "#/$defs/choices" }
|
|
178
|
+
},
|
|
179
|
+
"additionalProperties": false
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"required": ["choices"],
|
|
183
|
+
"properties": {
|
|
184
|
+
"id": { "$ref": "#/$defs/id" },
|
|
185
|
+
"type": { "const": "multi_choice" },
|
|
186
|
+
"label": { "$ref": "#/$defs/label" },
|
|
187
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
188
|
+
"required": { "$ref": "#/$defs/required" },
|
|
189
|
+
"choices": { "$ref": "#/$defs/choices" },
|
|
190
|
+
"min": { "type": "integer", "minimum": 0, "maximum": 50 },
|
|
191
|
+
"max": { "type": "integer", "minimum": 0, "maximum": 50 }
|
|
192
|
+
},
|
|
193
|
+
"additionalProperties": false
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"properties": {
|
|
197
|
+
"id": { "$ref": "#/$defs/id" },
|
|
198
|
+
"type": { "const": "checkbox" },
|
|
199
|
+
"label": { "$ref": "#/$defs/label" },
|
|
200
|
+
"hint": { "$ref": "#/$defs/hint" },
|
|
201
|
+
"required": { "$ref": "#/$defs/required" }
|
|
202
|
+
},
|
|
203
|
+
"additionalProperties": false
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"properties": {
|
|
207
|
+
"id": { "$ref": "#/$defs/id" },
|
|
208
|
+
"type": { "const": "consent" },
|
|
209
|
+
"label": { "$ref": "#/$defs/consentLabel" },
|
|
210
|
+
"hint": { "$ref": "#/$defs/hint" }
|
|
211
|
+
},
|
|
212
|
+
"additionalProperties": false
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|