@stacksjs/defaults 0.70.380 → 0.71.2
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/app/Actions/Auth/MagicLinkConsumeAction.ts +53 -0
- package/app/Actions/Auth/MagicLinkSendAction.ts +35 -0
- package/app/Actions/Cms/SitemapAction.ts +23 -2
- package/app/Jobs/PublishScheduledPagesJob.ts +26 -0
- package/app/Middleware/Site.ts +14 -0
- package/app/Middleware.ts +1 -0
- package/app/Models/Automation.ts +23 -0
- package/app/Models/AutomationRun.ts +26 -0
- package/app/Models/Campaign.ts +67 -3
- package/app/Models/CampaignSend.ts +90 -6
- package/app/Models/CampaignVariant.ts +25 -0
- package/app/Models/CommunicationSuppression.ts +22 -0
- package/app/Models/ConsentEvent.ts +26 -0
- package/app/Models/Content/Menu.ts +56 -0
- package/app/Models/Content/MenuItem.ts +91 -0
- package/app/Models/Content/Page.ts +111 -14
- package/app/Models/Content/PageRevision.ts +86 -0
- package/app/Models/Content/Post.ts +24 -9
- package/app/Models/Content/Redirect.ts +80 -0
- package/app/Models/Forms/Form.ts +91 -0
- package/app/Models/Forms/FormField.ts +127 -0
- package/app/Models/Forms/FormSubmission.ts +114 -0
- package/app/Models/MagicLinkToken.ts +97 -0
- package/app/Models/SenderDomain.ts +22 -0
- package/app/Models/Site.ts +112 -0
- package/app/Models/SiteDomain.ts +74 -0
- package/app/Models/SmsOptOut.ts +65 -0
- package/app/Models/UsageEvent.ts +24 -0
- package/app/Models/commerce/Auction.ts +173 -0
- package/app/Models/commerce/AuctionItem.ts +204 -0
- package/app/Models/commerce/Bid.ts +129 -0
- package/app/Models/commerce/Pledge.ts +112 -0
- package/bootstrap.ts +7 -0
- package/functions/public-application-url.ts +1 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/functions/dashboard/sidebar.ts +109 -3
- package/resources/functions/dashboard/toggles.ts +90 -2
- package/resources/views/cms/blocks/columns.stx +10 -0
- package/resources/views/cms/blocks/cta.stx +8 -0
- package/resources/views/cms/blocks/embed.stx +14 -0
- package/resources/views/cms/blocks/form.stx +132 -0
- package/resources/views/cms/blocks/hero.stx +16 -0
- package/resources/views/cms/blocks/image.stx +7 -0
- package/resources/views/cms/blocks/rich-text.stx +4 -0
- package/resources/views/cms/page.stx +28 -0
- package/routes/auth.ts +7 -0
- package/routes/forms.ts +110 -0
- package/views/auth/magic/[token].stx +88 -0
- package/views/dashboard/.discovered-models.json +46 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One entry in a Menu: either a link to a Page (`pageId`) or a raw `url` -
|
|
6
|
+
* exactly one of the two. Nests one level via `parentId` for dropdowns.
|
|
7
|
+
*/
|
|
8
|
+
export default defineModel({
|
|
9
|
+
name: 'MenuItem',
|
|
10
|
+
table: 'menu_items',
|
|
11
|
+
primaryKey: 'id',
|
|
12
|
+
autoIncrement: true,
|
|
13
|
+
|
|
14
|
+
traits: {
|
|
15
|
+
useTimestamps: true,
|
|
16
|
+
useApi: {
|
|
17
|
+
middleware: ['auth'],
|
|
18
|
+
uri: 'menu-items',
|
|
19
|
+
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
belongsTo: ['Menu', 'Page'],
|
|
24
|
+
|
|
25
|
+
attributes: {
|
|
26
|
+
label: {
|
|
27
|
+
required: true,
|
|
28
|
+
order: 1,
|
|
29
|
+
fillable: true,
|
|
30
|
+
validation: {
|
|
31
|
+
rule: schema.string().min(1).max(120),
|
|
32
|
+
},
|
|
33
|
+
factory: faker => faker.lorem.words(2),
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/** External or hand-written link. Null when the item points at a Page. */
|
|
37
|
+
url: {
|
|
38
|
+
required: false,
|
|
39
|
+
order: 2,
|
|
40
|
+
fillable: true,
|
|
41
|
+
validation: {
|
|
42
|
+
rule: schema.string().max(2048),
|
|
43
|
+
},
|
|
44
|
+
factory: () => null,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
target: {
|
|
48
|
+
required: false,
|
|
49
|
+
order: 3,
|
|
50
|
+
fillable: true,
|
|
51
|
+
default: '_self',
|
|
52
|
+
validation: {
|
|
53
|
+
rule: schema.enum(['_self', '_blank'] as const),
|
|
54
|
+
},
|
|
55
|
+
factory: () => '_self',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/** Dropdown parent within the same menu. */
|
|
59
|
+
parentId: {
|
|
60
|
+
required: false,
|
|
61
|
+
order: 4,
|
|
62
|
+
fillable: true,
|
|
63
|
+
validation: {
|
|
64
|
+
rule: schema.number().min(1),
|
|
65
|
+
},
|
|
66
|
+
factory: () => null,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
position: {
|
|
70
|
+
required: false,
|
|
71
|
+
order: 5,
|
|
72
|
+
fillable: true,
|
|
73
|
+
default: 0,
|
|
74
|
+
validation: {
|
|
75
|
+
rule: schema.number().min(0),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** `auth` items render only for signed-in visitors (portal links). */
|
|
80
|
+
visibility: {
|
|
81
|
+
required: false,
|
|
82
|
+
order: 6,
|
|
83
|
+
fillable: true,
|
|
84
|
+
default: 'public',
|
|
85
|
+
validation: {
|
|
86
|
+
rule: schema.enum(['public', 'auth'] as const),
|
|
87
|
+
},
|
|
88
|
+
factory: () => 'public',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
} as const)
|
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
import { defineModel } from '@stacksjs/orm'
|
|
2
2
|
import { schema } from '@stacksjs/validation'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A real CMS page: a block document served publicly at `path` on its site.
|
|
6
|
+
*
|
|
7
|
+
* `blocks` is the content - an ordered JSON array validated against the block
|
|
8
|
+
* registry (`@stacksjs/cms` `validateBlocks`). One JSON column rather than
|
|
9
|
+
* Section/Block rows so a page edit is one atomic write and a revision is a
|
|
10
|
+
* row copy, not a snapshot-of-a-join.
|
|
11
|
+
*
|
|
12
|
+
* The `useApi` surface is the ADMIN surface (auth'd on both sides - the table
|
|
13
|
+
* now carries draft content, and a public read route is how drafts leak).
|
|
14
|
+
* Public visitors get pages through the stx servers' CMS fallback, which only
|
|
15
|
+
* ever serves `status = 'published'` rows for the request's site.
|
|
16
|
+
*/
|
|
4
17
|
export default defineModel({
|
|
5
18
|
name: 'Page',
|
|
6
19
|
table: 'pages',
|
|
7
20
|
primaryKey: 'id',
|
|
8
21
|
autoIncrement: true,
|
|
9
22
|
|
|
23
|
+
indexes: [
|
|
24
|
+
{
|
|
25
|
+
name: 'pages_site_path_unique',
|
|
26
|
+
columns: ['site_id', 'path'],
|
|
27
|
+
unique: true,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
|
|
10
31
|
traits: {
|
|
11
32
|
useUuid: true,
|
|
12
33
|
useTimestamps: true,
|
|
13
34
|
useSearch: {
|
|
14
|
-
displayable: ['id', 'title', '
|
|
15
|
-
searchable: ['title', '
|
|
16
|
-
sortable: ['views', 'conversions'],
|
|
17
|
-
filterable: ['template'],
|
|
35
|
+
displayable: ['id', 'title', 'slug', 'path', 'template', 'status', 'views'],
|
|
36
|
+
searchable: ['title', 'slug', 'path'],
|
|
37
|
+
sortable: ['views', 'conversions', 'publishedAt'],
|
|
38
|
+
filterable: ['template', 'status'],
|
|
18
39
|
},
|
|
19
40
|
useApi: {
|
|
20
|
-
|
|
21
|
-
// write. Declared explicitly because the trait now defaults BOTH sides to
|
|
22
|
-
// `auth` — an undeclared read route is how a customer list leaks
|
|
23
|
-
// (stacksjs/stacks#2224). Behaviour here is unchanged.
|
|
24
|
-
middleware: { read: [], write: ['auth'] },
|
|
41
|
+
middleware: ['auth'],
|
|
25
42
|
uri: 'pages',
|
|
26
43
|
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
27
44
|
},
|
|
@@ -31,7 +48,7 @@ export default defineModel({
|
|
|
31
48
|
},
|
|
32
49
|
},
|
|
33
50
|
|
|
34
|
-
belongsTo: ['Author'],
|
|
51
|
+
belongsTo: ['Author', 'Site'],
|
|
35
52
|
|
|
36
53
|
attributes: {
|
|
37
54
|
title: {
|
|
@@ -48,9 +65,46 @@ export default defineModel({
|
|
|
48
65
|
factory: faker => faker.lorem.sentence(),
|
|
49
66
|
},
|
|
50
67
|
|
|
68
|
+
/** Last path segment, unique among siblings. */
|
|
69
|
+
slug: {
|
|
70
|
+
required: false,
|
|
71
|
+
order: 2,
|
|
72
|
+
fillable: true,
|
|
73
|
+
validation: {
|
|
74
|
+
rule: schema.string().max(255),
|
|
75
|
+
},
|
|
76
|
+
factory: faker => faker.lorem.slug(),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Materialized full path (`/admissions/visit`), unique per site. Derived
|
|
81
|
+
* from the parent chain + slug on save; a slug change rewrites descendant
|
|
82
|
+
* paths and leaves Redirect rows behind.
|
|
83
|
+
*/
|
|
84
|
+
path: {
|
|
85
|
+
required: false,
|
|
86
|
+
order: 3,
|
|
87
|
+
fillable: true,
|
|
88
|
+
validation: {
|
|
89
|
+
rule: schema.string().max(2048),
|
|
90
|
+
},
|
|
91
|
+
factory: faker => `/${faker.lorem.slug()}`,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** Page-tree parent. Null for top-level pages. */
|
|
95
|
+
parentId: {
|
|
96
|
+
required: false,
|
|
97
|
+
order: 4,
|
|
98
|
+
fillable: true,
|
|
99
|
+
validation: {
|
|
100
|
+
rule: schema.number().min(1),
|
|
101
|
+
},
|
|
102
|
+
factory: () => null,
|
|
103
|
+
},
|
|
104
|
+
|
|
51
105
|
template: {
|
|
52
106
|
required: true,
|
|
53
|
-
order:
|
|
107
|
+
order: 5,
|
|
54
108
|
fillable: true,
|
|
55
109
|
validation: {
|
|
56
110
|
rule: schema.string().min(3),
|
|
@@ -61,9 +115,52 @@ export default defineModel({
|
|
|
61
115
|
factory: faker => faker.helpers.arrayElement(['default', 'landing', 'blog', 'contact']),
|
|
62
116
|
},
|
|
63
117
|
|
|
118
|
+
/** The block document. Ordered array of { id, type, props }. */
|
|
119
|
+
blocks: {
|
|
120
|
+
required: false,
|
|
121
|
+
order: 6,
|
|
122
|
+
fillable: true,
|
|
123
|
+
validation: {
|
|
124
|
+
rule: schema.json(),
|
|
125
|
+
},
|
|
126
|
+
factory: () => JSON.stringify([]),
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
metaDescription: {
|
|
130
|
+
required: false,
|
|
131
|
+
order: 7,
|
|
132
|
+
fillable: true,
|
|
133
|
+
validation: {
|
|
134
|
+
rule: schema.string().max(320),
|
|
135
|
+
},
|
|
136
|
+
factory: faker => faker.lorem.sentence(),
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
status: {
|
|
140
|
+
required: false,
|
|
141
|
+
order: 8,
|
|
142
|
+
fillable: true,
|
|
143
|
+
default: 'draft',
|
|
144
|
+
validation: {
|
|
145
|
+
rule: schema.enum(['draft', 'published', 'scheduled', 'archived'] as const),
|
|
146
|
+
},
|
|
147
|
+
factory: faker => faker.helpers.arrayElement(['draft', 'published']),
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** When a `scheduled` page goes live - enforced by the publish job. */
|
|
151
|
+
scheduledAt: {
|
|
152
|
+
required: false,
|
|
153
|
+
order: 9,
|
|
154
|
+
fillable: true,
|
|
155
|
+
validation: {
|
|
156
|
+
rule: schema.timestamp(),
|
|
157
|
+
},
|
|
158
|
+
factory: () => null,
|
|
159
|
+
},
|
|
160
|
+
|
|
64
161
|
views: {
|
|
65
162
|
required: false,
|
|
66
|
-
order:
|
|
163
|
+
order: 10,
|
|
67
164
|
fillable: true,
|
|
68
165
|
default: 0,
|
|
69
166
|
validation: {
|
|
@@ -77,7 +174,7 @@ export default defineModel({
|
|
|
77
174
|
|
|
78
175
|
publishedAt: {
|
|
79
176
|
required: false,
|
|
80
|
-
order:
|
|
177
|
+
order: 11,
|
|
81
178
|
fillable: true,
|
|
82
179
|
validation: {
|
|
83
180
|
rule: schema.timestamp(),
|
|
@@ -93,7 +190,7 @@ export default defineModel({
|
|
|
93
190
|
|
|
94
191
|
conversions: {
|
|
95
192
|
required: false,
|
|
96
|
-
order:
|
|
193
|
+
order: 12,
|
|
97
194
|
fillable: true,
|
|
98
195
|
default: 0,
|
|
99
196
|
validation: {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A snapshot of a Page's document taken just before an overwrite - restoring
|
|
6
|
+
* is copying the snapshot back and recording a new revision of what it
|
|
7
|
+
* replaced. One row per save (the previous state), pruned to the newest N per
|
|
8
|
+
* page (`config.cms.revisions.keep`).
|
|
9
|
+
*
|
|
10
|
+
* No `useApi`: revisions are read and restored through the CMS actions, never
|
|
11
|
+
* CRUD'd directly.
|
|
12
|
+
*/
|
|
13
|
+
export default defineModel({
|
|
14
|
+
name: 'PageRevision',
|
|
15
|
+
table: 'page_revisions',
|
|
16
|
+
primaryKey: 'id',
|
|
17
|
+
autoIncrement: true,
|
|
18
|
+
|
|
19
|
+
indexes: [
|
|
20
|
+
{
|
|
21
|
+
name: 'page_revisions_page_revision_unique',
|
|
22
|
+
columns: ['page_id', 'revision'],
|
|
23
|
+
unique: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
traits: {
|
|
28
|
+
useTimestamps: true,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
belongsTo: ['Page', 'Author'],
|
|
32
|
+
|
|
33
|
+
attributes: {
|
|
34
|
+
/** Monotonic per page, starting at 1. */
|
|
35
|
+
revision: {
|
|
36
|
+
required: true,
|
|
37
|
+
order: 1,
|
|
38
|
+
fillable: true,
|
|
39
|
+
validation: {
|
|
40
|
+
rule: schema.number().min(1),
|
|
41
|
+
},
|
|
42
|
+
factory: faker => faker.number.int({ min: 1, max: 20 }),
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
title: {
|
|
46
|
+
required: true,
|
|
47
|
+
order: 2,
|
|
48
|
+
fillable: true,
|
|
49
|
+
validation: {
|
|
50
|
+
rule: schema.string().max(255),
|
|
51
|
+
},
|
|
52
|
+
factory: faker => faker.lorem.sentence(),
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
blocks: {
|
|
56
|
+
required: false,
|
|
57
|
+
order: 3,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.json(),
|
|
61
|
+
},
|
|
62
|
+
factory: () => JSON.stringify([]),
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
metaDescription: {
|
|
66
|
+
required: false,
|
|
67
|
+
order: 4,
|
|
68
|
+
fillable: true,
|
|
69
|
+
validation: {
|
|
70
|
+
rule: schema.string().max(320),
|
|
71
|
+
},
|
|
72
|
+
factory: () => null,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/** Editor-supplied note ("reworded hero for spring"), optional. */
|
|
76
|
+
note: {
|
|
77
|
+
required: false,
|
|
78
|
+
order: 5,
|
|
79
|
+
fillable: true,
|
|
80
|
+
validation: {
|
|
81
|
+
rule: schema.string().max(500),
|
|
82
|
+
},
|
|
83
|
+
factory: () => null,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
} as const)
|
|
@@ -11,9 +11,11 @@ export default defineModel({
|
|
|
11
11
|
useUuid: true,
|
|
12
12
|
useTimestamps: true,
|
|
13
13
|
useSearch: {
|
|
14
|
-
displayable: ['id', 'title', 'author', 'views', 'status', 'poster', 'focusKeyword', 'metaDescription', 'canonicalUrl'],
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
displayable: ['id', 'title', 'slug', 'author', 'views', 'status', 'poster', 'focusKeyword', 'metaDescription', 'canonicalUrl'],
|
|
15
|
+
// `content`, not `body` - the column is `content`, and the old spelling
|
|
16
|
+
// silently indexed nothing. `comments` likewise was never a column.
|
|
17
|
+
searchable: ['title', 'slug', 'author', 'content', 'excerpt', 'focusKeyword', 'metaDescription'],
|
|
18
|
+
sortable: ['published_at', 'views'],
|
|
17
19
|
filterable: ['status'],
|
|
18
20
|
},
|
|
19
21
|
|
|
@@ -24,17 +26,16 @@ export default defineModel({
|
|
|
24
26
|
// trait targets the real `commentables` table, activating it is correct.
|
|
25
27
|
commentable: true,
|
|
26
28
|
useApi: {
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
middleware: { read: [], write: ['auth'] },
|
|
29
|
+
// Admin surface now: the table carries drafts, and a public read route
|
|
30
|
+
// is how drafts leak. Public visitors get published posts through the
|
|
31
|
+
// site's own routes/pages, which filter by status themselves.
|
|
32
|
+
middleware: ['auth'],
|
|
32
33
|
uri: 'posts',
|
|
33
34
|
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
34
35
|
},
|
|
35
36
|
},
|
|
36
37
|
|
|
37
|
-
belongsTo: ['Author'],
|
|
38
|
+
belongsTo: ['Author', 'Site'],
|
|
38
39
|
belongsToMany: {
|
|
39
40
|
categories: {
|
|
40
41
|
model: 'Category',
|
|
@@ -78,6 +79,20 @@ export default defineModel({
|
|
|
78
79
|
},
|
|
79
80
|
factory: faker => faker.lorem.sentence(),
|
|
80
81
|
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* URL identity: `/news/{slug}` beats `/blog/{id}` for a public site.
|
|
85
|
+
* Nullable for pre-slug rows; the RSS/sitemap actions fall back to id.
|
|
86
|
+
*/
|
|
87
|
+
slug: {
|
|
88
|
+
required: false,
|
|
89
|
+
order: 2,
|
|
90
|
+
fillable: true,
|
|
91
|
+
validation: {
|
|
92
|
+
rule: schema.string().max(255),
|
|
93
|
+
},
|
|
94
|
+
factory: faker => faker.lorem.slug(),
|
|
95
|
+
},
|
|
81
96
|
poster: {
|
|
82
97
|
required: false,
|
|
83
98
|
order: 4,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A path-level redirect on a site. Written automatically when a page's slug
|
|
6
|
+
* or ancestry changes (`source: 'slug-change'`) so old links keep working,
|
|
7
|
+
* and manually from the dashboard (`source: 'manual'`). Resolved by the CMS
|
|
8
|
+
* fallback after page lookup misses.
|
|
9
|
+
*/
|
|
10
|
+
export default defineModel({
|
|
11
|
+
name: 'Redirect',
|
|
12
|
+
table: 'redirects',
|
|
13
|
+
primaryKey: 'id',
|
|
14
|
+
autoIncrement: true,
|
|
15
|
+
|
|
16
|
+
indexes: [
|
|
17
|
+
{
|
|
18
|
+
name: 'redirects_site_from_unique',
|
|
19
|
+
columns: ['site_id', 'from_path'],
|
|
20
|
+
unique: true,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
traits: {
|
|
25
|
+
useTimestamps: true,
|
|
26
|
+
useApi: {
|
|
27
|
+
middleware: ['auth'],
|
|
28
|
+
uri: 'redirects',
|
|
29
|
+
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
belongsTo: ['Site'],
|
|
34
|
+
|
|
35
|
+
attributes: {
|
|
36
|
+
fromPath: {
|
|
37
|
+
required: true,
|
|
38
|
+
order: 1,
|
|
39
|
+
fillable: true,
|
|
40
|
+
validation: {
|
|
41
|
+
rule: schema.string().max(2048),
|
|
42
|
+
},
|
|
43
|
+
factory: faker => `/${faker.lorem.slug()}`,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
toPath: {
|
|
47
|
+
required: true,
|
|
48
|
+
order: 2,
|
|
49
|
+
fillable: true,
|
|
50
|
+
validation: {
|
|
51
|
+
rule: schema.string().max(2048),
|
|
52
|
+
},
|
|
53
|
+
factory: faker => `/${faker.lorem.slug()}`,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
statusCode: {
|
|
57
|
+
required: false,
|
|
58
|
+
order: 3,
|
|
59
|
+
fillable: true,
|
|
60
|
+
default: 301,
|
|
61
|
+
validation: {
|
|
62
|
+
// schema.enum is string-typed; the numeric range pins 301/302 (the
|
|
63
|
+
// read path coerces anything else to 301 anyway).
|
|
64
|
+
rule: schema.number().min(301).max(302),
|
|
65
|
+
},
|
|
66
|
+
factory: () => 301,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
source: {
|
|
70
|
+
required: false,
|
|
71
|
+
order: 4,
|
|
72
|
+
fillable: true,
|
|
73
|
+
default: 'manual',
|
|
74
|
+
validation: {
|
|
75
|
+
rule: schema.enum(['slug-change', 'manual'] as const),
|
|
76
|
+
},
|
|
77
|
+
factory: () => 'manual',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
} as const)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A user-defined form: inquiry, permission slip, event registration,
|
|
6
|
+
* donation. Fields are FormField rows; submissions are FormSubmission rows.
|
|
7
|
+
*
|
|
8
|
+
* The `useApi` surface is the ADMIN builder surface. The public render and
|
|
9
|
+
* submit endpoints live in `@stacksjs/forms` routes, keyed by uuid, and only
|
|
10
|
+
* ever serve `status: 'active'` forms.
|
|
11
|
+
*/
|
|
12
|
+
export default defineModel({
|
|
13
|
+
name: 'Form',
|
|
14
|
+
table: 'forms',
|
|
15
|
+
primaryKey: 'id',
|
|
16
|
+
autoIncrement: true,
|
|
17
|
+
|
|
18
|
+
indexes: [
|
|
19
|
+
{
|
|
20
|
+
name: 'forms_site_handle_unique',
|
|
21
|
+
columns: ['site_id', 'handle'],
|
|
22
|
+
unique: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
traits: {
|
|
27
|
+
useUuid: true,
|
|
28
|
+
useTimestamps: true,
|
|
29
|
+
useApi: {
|
|
30
|
+
middleware: ['auth'],
|
|
31
|
+
uri: 'forms',
|
|
32
|
+
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
33
|
+
},
|
|
34
|
+
useSearch: {
|
|
35
|
+
displayable: ['id', 'name', 'handle', 'status'],
|
|
36
|
+
searchable: ['name', 'handle'],
|
|
37
|
+
filterable: ['status'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
belongsTo: ['Site'],
|
|
42
|
+
hasMany: ['FormField', 'FormSubmission'],
|
|
43
|
+
|
|
44
|
+
attributes: {
|
|
45
|
+
name: {
|
|
46
|
+
required: true,
|
|
47
|
+
order: 1,
|
|
48
|
+
fillable: true,
|
|
49
|
+
validation: {
|
|
50
|
+
rule: schema.string().min(2).max(255),
|
|
51
|
+
},
|
|
52
|
+
factory: faker => `${faker.lorem.words(2)} form`,
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
handle: {
|
|
56
|
+
required: true,
|
|
57
|
+
order: 2,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.string().min(2).max(64).matches(/^[a-z0-9-]+$/),
|
|
61
|
+
},
|
|
62
|
+
factory: faker => faker.lorem.slug(),
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
status: {
|
|
66
|
+
required: false,
|
|
67
|
+
order: 3,
|
|
68
|
+
fillable: true,
|
|
69
|
+
default: 'draft',
|
|
70
|
+
validation: {
|
|
71
|
+
rule: schema.enum(['draft', 'active', 'closed'] as const),
|
|
72
|
+
},
|
|
73
|
+
factory: () => 'active',
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* JSON settings: { submitLabel?, confirmation: { type: 'message'|'redirect',
|
|
78
|
+
* value }, notifyEmails: string[], emailField?, nameField?,
|
|
79
|
+
* payment?: { mode: 'fixed'|'user_amount'|'field_sum', amountCents?, currency? } }
|
|
80
|
+
*/
|
|
81
|
+
settings: {
|
|
82
|
+
required: false,
|
|
83
|
+
order: 4,
|
|
84
|
+
fillable: true,
|
|
85
|
+
validation: {
|
|
86
|
+
rule: schema.json(),
|
|
87
|
+
},
|
|
88
|
+
factory: () => JSON.stringify({}),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
} as const)
|