@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.
Files changed (50) hide show
  1. package/app/Actions/Auth/MagicLinkConsumeAction.ts +53 -0
  2. package/app/Actions/Auth/MagicLinkSendAction.ts +35 -0
  3. package/app/Actions/Cms/SitemapAction.ts +23 -2
  4. package/app/Jobs/PublishScheduledPagesJob.ts +26 -0
  5. package/app/Middleware/Site.ts +14 -0
  6. package/app/Middleware.ts +1 -0
  7. package/app/Models/Automation.ts +23 -0
  8. package/app/Models/AutomationRun.ts +26 -0
  9. package/app/Models/Campaign.ts +67 -3
  10. package/app/Models/CampaignSend.ts +90 -6
  11. package/app/Models/CampaignVariant.ts +25 -0
  12. package/app/Models/CommunicationSuppression.ts +22 -0
  13. package/app/Models/ConsentEvent.ts +26 -0
  14. package/app/Models/Content/Menu.ts +56 -0
  15. package/app/Models/Content/MenuItem.ts +91 -0
  16. package/app/Models/Content/Page.ts +111 -14
  17. package/app/Models/Content/PageRevision.ts +86 -0
  18. package/app/Models/Content/Post.ts +24 -9
  19. package/app/Models/Content/Redirect.ts +80 -0
  20. package/app/Models/Forms/Form.ts +91 -0
  21. package/app/Models/Forms/FormField.ts +127 -0
  22. package/app/Models/Forms/FormSubmission.ts +114 -0
  23. package/app/Models/MagicLinkToken.ts +97 -0
  24. package/app/Models/SenderDomain.ts +22 -0
  25. package/app/Models/Site.ts +112 -0
  26. package/app/Models/SiteDomain.ts +74 -0
  27. package/app/Models/SmsOptOut.ts +65 -0
  28. package/app/Models/UsageEvent.ts +24 -0
  29. package/app/Models/commerce/Auction.ts +173 -0
  30. package/app/Models/commerce/AuctionItem.ts +204 -0
  31. package/app/Models/commerce/Bid.ts +129 -0
  32. package/app/Models/commerce/Pledge.ts +112 -0
  33. package/bootstrap.ts +7 -0
  34. package/functions/public-application-url.ts +1 -1
  35. package/ide/vscode/package.json +1 -1
  36. package/package.json +2 -2
  37. package/resources/functions/dashboard/sidebar.ts +109 -3
  38. package/resources/functions/dashboard/toggles.ts +90 -2
  39. package/resources/views/cms/blocks/columns.stx +10 -0
  40. package/resources/views/cms/blocks/cta.stx +8 -0
  41. package/resources/views/cms/blocks/embed.stx +14 -0
  42. package/resources/views/cms/blocks/form.stx +132 -0
  43. package/resources/views/cms/blocks/hero.stx +16 -0
  44. package/resources/views/cms/blocks/image.stx +7 -0
  45. package/resources/views/cms/blocks/rich-text.stx +4 -0
  46. package/resources/views/cms/page.stx +28 -0
  47. package/routes/auth.ts +7 -0
  48. package/routes/forms.ts +110 -0
  49. package/views/auth/magic/[token].stx +88 -0
  50. 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', 'author', 'template', 'views', 'conversions'],
15
- searchable: ['title', 'author', 'template'],
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
- // Public catalog: anyone may browse, only authenticated callers may
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: 3,
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: 4,
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: 6,
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: 5,
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
- searchable: ['title', 'author', 'body', 'excerpt', 'focusKeyword', 'metaDescription'],
16
- sortable: ['published_at', 'views', 'comments'],
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
- // Public catalog: anyone may browse, only authenticated callers may
28
- // write. Declared explicitly because the trait now defaults BOTH sides to
29
- // `auth` an undeclared read route is how a customer list leaks
30
- // (stacksjs/stacks#2224). Behaviour here is unchanged.
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)