@stacksjs/orm 0.72.16 → 0.72.18

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 (102) hide show
  1. package/dist/index.d.ts +97 -97
  2. package/dist/index.js +7 -7
  3. package/dist/models/Activity.ts +95 -0
  4. package/dist/models/AnalyticsEvent.ts +70 -0
  5. package/dist/models/Automation.ts +23 -0
  6. package/dist/models/AutomationRun.ts +26 -0
  7. package/dist/models/Board.ts +104 -0
  8. package/dist/models/BoardColumn.ts +88 -0
  9. package/dist/models/Campaign.ts +321 -0
  10. package/dist/models/CampaignSend.ts +191 -0
  11. package/dist/models/CampaignVariant.ts +25 -0
  12. package/dist/models/Card.ts +118 -0
  13. package/dist/models/CardComment.ts +68 -0
  14. package/dist/models/Comment.ts +109 -0
  15. package/dist/models/CommunicationSuppression.ts +22 -0
  16. package/dist/models/ConsentEvent.ts +30 -0
  17. package/dist/models/Content/Author.ts +109 -0
  18. package/dist/models/Content/Menu.ts +56 -0
  19. package/dist/models/Content/MenuItem.ts +91 -0
  20. package/dist/models/Content/Page.ts +205 -0
  21. package/dist/models/Content/PageRevision.ts +86 -0
  22. package/dist/models/Content/Post.ts +230 -0
  23. package/dist/models/Content/Redirect.ts +80 -0
  24. package/dist/models/Deployment.ts +117 -0
  25. package/dist/models/EmailIdempotency.ts +64 -0
  26. package/dist/models/EmailList.ts +134 -0
  27. package/dist/models/EmailListSubscriber.ts +91 -0
  28. package/dist/models/EmailSuppression.ts +52 -0
  29. package/dist/models/EmailWebhookEvent.ts +51 -0
  30. package/dist/models/Error.ts +77 -0
  31. package/dist/models/FailedJob.ts +84 -0
  32. package/dist/models/Forms/Form.ts +91 -0
  33. package/dist/models/Forms/FormField.ts +127 -0
  34. package/dist/models/Forms/FormSubmission.ts +114 -0
  35. package/dist/models/Job.ts +69 -0
  36. package/dist/models/Label.ts +67 -0
  37. package/dist/models/Log.ts +134 -0
  38. package/dist/models/MagicLinkToken.ts +97 -0
  39. package/dist/models/MailPreference.ts +213 -0
  40. package/dist/models/Notification.ts +68 -0
  41. package/dist/models/NotificationDelivery.ts +102 -0
  42. package/dist/models/PaymentMethod.ts +98 -0
  43. package/dist/models/PaymentProduct.ts +91 -0
  44. package/dist/models/PaymentTransaction.ts +78 -0
  45. package/dist/models/QueryLog.ts +182 -0
  46. package/dist/models/README.md +31 -0
  47. package/dist/models/Release.ts +90 -0
  48. package/dist/models/Request.ts +133 -0
  49. package/dist/models/SenderDomain.ts +22 -0
  50. package/dist/models/Site.ts +112 -0
  51. package/dist/models/SiteDomain.ts +74 -0
  52. package/dist/models/SmsOptOut.ts +65 -0
  53. package/dist/models/SocialAccount.ts +70 -0
  54. package/dist/models/SocialPost.ts +144 -0
  55. package/dist/models/Subscriber.ts +89 -0
  56. package/dist/models/SubscriberEmail.ts +55 -0
  57. package/dist/models/Subscription.ts +153 -0
  58. package/dist/models/Tag.ts +105 -0
  59. package/dist/models/Team.ts +75 -0
  60. package/dist/models/TeamInvitation.ts +159 -0
  61. package/dist/models/TeamMember.ts +88 -0
  62. package/dist/models/UsageEvent.ts +24 -0
  63. package/dist/models/User.ts +142 -0
  64. package/dist/models/commerce/Auction.ts +173 -0
  65. package/dist/models/commerce/AuctionItem.ts +204 -0
  66. package/dist/models/commerce/Bid.ts +129 -0
  67. package/dist/models/commerce/Cart.ts +140 -0
  68. package/dist/models/commerce/CartItem.ts +138 -0
  69. package/dist/models/commerce/Category.ts +125 -0
  70. package/dist/models/commerce/Coupon.ts +186 -0
  71. package/dist/models/commerce/Customer.ts +132 -0
  72. package/dist/models/commerce/DeliveryRoute.ts +127 -0
  73. package/dist/models/commerce/DeliveryStop.ts +166 -0
  74. package/dist/models/commerce/DigitalDelivery.ts +106 -0
  75. package/dist/models/commerce/Driver.ts +168 -0
  76. package/dist/models/commerce/DriverPing.ts +103 -0
  77. package/dist/models/commerce/GiftCard.ts +195 -0
  78. package/dist/models/commerce/LicenseKey.ts +89 -0
  79. package/dist/models/commerce/LoyaltyPoint.ts +108 -0
  80. package/dist/models/commerce/LoyaltyReward.ts +129 -0
  81. package/dist/models/commerce/Manufacturer.ts +90 -0
  82. package/dist/models/commerce/Order.ts +198 -0
  83. package/dist/models/commerce/OrderIdempotency.ts +34 -0
  84. package/dist/models/commerce/OrderItem.ts +51 -0
  85. package/dist/models/commerce/Payment.ts +182 -0
  86. package/dist/models/commerce/Pledge.ts +112 -0
  87. package/dist/models/commerce/PrintDevice.ts +120 -0
  88. package/dist/models/commerce/Product.ts +153 -0
  89. package/dist/models/commerce/ProductUnit.ts +113 -0
  90. package/dist/models/commerce/ProductVariant.ts +111 -0
  91. package/dist/models/commerce/Receipt.ts +144 -0
  92. package/dist/models/commerce/Review.ts +171 -0
  93. package/dist/models/commerce/ShippingMethod.ts +102 -0
  94. package/dist/models/commerce/ShippingRate.ts +79 -0
  95. package/dist/models/commerce/ShippingZone.ts +116 -0
  96. package/dist/models/commerce/TaxRate.ts +167 -0
  97. package/dist/models/commerce/Transaction.ts +127 -0
  98. package/dist/models/commerce/WaitlistProduct.ts +151 -0
  99. package/dist/models/commerce/WaitlistRestaurant.ts +189 -0
  100. package/dist/models/realtime/Websocket.ts +78 -0
  101. package/dist/password-policy.ts +46 -0
  102. package/package.json +4 -4
@@ -0,0 +1,68 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * Card comment for the dashboard Kanban surface.
6
+ *
7
+ * Each comment belongs to a card and (optionally) to the user who
8
+ * wrote it. Anonymous comments are allowed via nullable `user_id` —
9
+ * makes the dev dashboard usable on localhost without an auth round-
10
+ * trip, and supports system-generated activity entries.
11
+ *
12
+ * Distinct from `Comment` (which is post-specific) so the schemas
13
+ * don't fight: post comments carry `author_name`/`author_email` for
14
+ * public commenters; card comments are internal-only and lean on
15
+ * the `user_id` FK for attribution.
16
+ */
17
+ export default defineModel({
18
+ name: 'CardComment',
19
+ table: 'card_comments',
20
+ primaryKey: 'id',
21
+ autoIncrement: true,
22
+
23
+ traits: {
24
+ useUuid: true,
25
+ useTimestamps: true,
26
+ useSearch: {
27
+ displayable: ['id', 'cardId', 'userId', 'body', 'createdAt'],
28
+ searchable: ['body'],
29
+ sortable: ['createdAt'],
30
+ filterable: ['cardId'],
31
+ },
32
+
33
+ useSeeder: { count: 0 },
34
+
35
+ useApi: {
36
+ uri: 'card-comments',
37
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
38
+ middleware: ['auth'],
39
+ },
40
+ },
41
+
42
+ belongsTo: ['Card', 'User'],
43
+
44
+ attributes: {
45
+ cardId: {
46
+ order: 1,
47
+ fillable: true,
48
+ validation: { rule: schema.number().required() },
49
+ factory: () => 1,
50
+ },
51
+
52
+ userId: {
53
+ order: 2,
54
+ fillable: true,
55
+ validation: { rule: schema.number() },
56
+ factory: () => null,
57
+ },
58
+
59
+ body: {
60
+ order: 3,
61
+ fillable: true,
62
+ validation: { rule: schema.string().required().min(1).max(10000) },
63
+ factory: faker => faker.lorem.sentence(),
64
+ },
65
+ },
66
+
67
+ dashboard: { enabled: false },
68
+ } as const)
@@ -0,0 +1,109 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'Comment',
6
+ table: 'comments',
7
+ primaryKey: 'id',
8
+ autoIncrement: true,
9
+
10
+ traits: {
11
+ useUuid: true,
12
+ useTimestamps: true,
13
+ useSeeder: {
14
+ count: 25,
15
+ },
16
+ useApi: {
17
+ uri: 'comments',
18
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
19
+ middleware: ['auth'],
20
+ },
21
+ },
22
+
23
+ belongsTo: ['Post', 'User'],
24
+
25
+ attributes: {
26
+ authorName: {
27
+ required: true,
28
+ fillable: true,
29
+ validation: {
30
+ rule: schema.string().min(2).max(100),
31
+ },
32
+ factory: faker => faker.person.fullName(),
33
+ },
34
+
35
+ authorEmail: {
36
+ required: true,
37
+ fillable: true,
38
+ validation: {
39
+ rule: schema.string().email(),
40
+ },
41
+ factory: faker => faker.internet.email(),
42
+ },
43
+
44
+ content: {
45
+ required: true,
46
+ fillable: true,
47
+ validation: {
48
+ rule: schema.string().min(1).max(2000),
49
+ },
50
+ factory: faker => faker.lorem.paragraph(),
51
+ },
52
+
53
+ body: {
54
+ required: false,
55
+ fillable: true,
56
+ validation: {
57
+ rule: schema.string().max(2000),
58
+ },
59
+ factory: faker => faker.lorem.paragraph(),
60
+ },
61
+
62
+ postTitle: {
63
+ required: false,
64
+ fillable: true,
65
+ validation: {
66
+ rule: schema.string().max(255),
67
+ },
68
+ factory: faker => faker.lorem.sentence(),
69
+ },
70
+
71
+ status: {
72
+ required: true,
73
+ fillable: true,
74
+ default: 'pending',
75
+ validation: {
76
+ rule: schema.enum(['pending', 'approved', 'spam', 'trash']),
77
+ },
78
+ factory: faker => faker.helpers.arrayElement(['pending', 'approved', 'spam', 'approved', 'approved']),
79
+ },
80
+
81
+ ipAddress: {
82
+ required: false,
83
+ fillable: true,
84
+ validation: {
85
+ rule: schema.string().max(45),
86
+ },
87
+ factory: faker => faker.internet.ip(),
88
+ },
89
+
90
+ userAgent: {
91
+ required: false,
92
+ fillable: true,
93
+ validation: {
94
+ rule: schema.string().max(500),
95
+ },
96
+ factory: faker => faker.internet.userAgent(),
97
+ },
98
+
99
+ isApproved: {
100
+ required: false,
101
+ fillable: true,
102
+ default: 0,
103
+ validation: {
104
+ rule: schema.number(),
105
+ },
106
+ factory: faker => faker.number.int({ min: 0, max: 1 }),
107
+ },
108
+ },
109
+ } as const)
@@ -0,0 +1,22 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'CommunicationSuppression',
6
+ table: 'communication_suppressions',
7
+ belongsTo: ['Team'],
8
+ traits: {
9
+ useUuid: true,
10
+ useTimestamps: true,
11
+ useApi: { uri: 'communication-suppressions', routes: ['index', 'store', 'show', 'destroy'], middleware: ['auth', 'team'] },
12
+ },
13
+ indexes: [{ name: 'communication_suppressions_unique', columns: ['team_id', 'channel', 'recipient'], unique: true }],
14
+ attributes: {
15
+ recipient: { required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.internet.email() },
16
+ channel: { required: true, fillable: true, validation: { rule: schema.enum(['email', 'sms', 'push']) }, factory: () => 'email' },
17
+ reason: { required: true, fillable: true, validation: { rule: schema.enum(['unsubscribe', 'bounce', 'complaint', 'carrier', 'manual', 'legal']) }, factory: () => 'unsubscribe' },
18
+ source: { required: true, fillable: true, validation: { rule: schema.string().max(120) }, factory: () => 'preference_center' },
19
+ suppressedAt: { required: true, fillable: true, validation: { rule: schema.timestamp() }, factory: () => new Date().toISOString() },
20
+ liftedAt: { required: false, fillable: false, validation: { rule: schema.timestamp() }, factory: () => null },
21
+ },
22
+ } as const)
@@ -0,0 +1,30 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'ConsentEvent',
6
+ table: 'consent_events',
7
+ belongsTo: ['Team'],
8
+ traits: {
9
+ useUuid: true,
10
+ useTimestamps: true,
11
+ useApi: { uri: 'consent-events', routes: ['index', 'show'], middleware: ['auth', 'team'] },
12
+ },
13
+ indexes: [
14
+ { name: 'consent_events_idempotency_unique', columns: ['team_id', 'idempotency_key'], unique: true },
15
+ { name: 'consent_events_lookup', columns: ['team_id', 'channel', 'recipient', 'occurred_at'] },
16
+ ],
17
+ attributes: {
18
+ recipient: { required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.internet.email() },
19
+ channel: { required: true, fillable: true, validation: { rule: schema.enum(['email', 'sms', 'push']) }, factory: () => 'email' },
20
+ action: { required: true, fillable: true, validation: { rule: schema.enum(['requested', 'granted', 'revoked', 'confirmed', 'suppressed']) }, factory: () => 'granted' },
21
+ purpose: { required: true, fillable: true, validation: { rule: schema.string().max(120) }, factory: () => 'marketing' },
22
+ source: { required: true, fillable: true, validation: { rule: schema.string().max(120) }, factory: () => 'signup_form' },
23
+ jurisdiction: { required: false, fillable: true, validation: { rule: schema.string().max(80) }, factory: () => null },
24
+ policyVersion: { required: true, fillable: true, validation: { rule: schema.string().max(40) }, factory: () => '1.0' },
25
+ idempotencyKey: { required: false, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.string.uuid() },
26
+ proof: { required: false, fillable: true, validation: { rule: schema.json() }, factory: () => JSON.stringify({}) },
27
+ ipAddress: { required: false, fillable: true, validation: { rule: schema.string().max(45) }, factory: faker => faker.internet.ip() },
28
+ occurredAt: { required: true, fillable: true, validation: { rule: schema.timestamp() }, factory: () => new Date().toISOString() },
29
+ },
30
+ } as const)
@@ -0,0 +1,109 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'Author', // defaults to the sanitized file name
6
+ table: 'authors', // defaults to the lowercase, plural name of the model name (or the name of the model file)
7
+ primaryKey: 'id', // defaults to `id`
8
+ autoIncrement: true, // defaults to true
9
+
10
+ // Define composite indexes
11
+ indexes: [
12
+ {
13
+ name: 'authors_email_name_index',
14
+ columns: ['email', 'name'],
15
+ },
16
+ ],
17
+
18
+ traits: {
19
+ useAuth: {
20
+ usePasskey: true,
21
+ },
22
+ useUuid: true,
23
+ useTimestamps: true, // defaults to true, `timestampable` used as an alias
24
+ useSearch: {
25
+ displayable: ['id', 'name', 'email'], // the fields to become d (defaults to all fields)
26
+ searchable: ['name', 'email'], // the fields to become searchable (defaults to all fields)
27
+ sortable: ['created_at', 'updated_at'], // the fields to become sortable (defaults to all fields)
28
+ filterable: [],
29
+ },
30
+
31
+ // No faker seeding. Blog authorship lives in markdown frontmatter
32
+ // (content/blog/*.md); this model backs the CMS dashboard only.
33
+
34
+ useApi: {
35
+ // Public catalog: anyone may browse, only authenticated callers may
36
+ // write. Declared explicitly because the trait now defaults BOTH sides to
37
+ // `auth` — an undeclared read route is how a customer list leaks
38
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
39
+ middleware: { read: [], write: ['auth'] },
40
+ uri: 'authors',
41
+
42
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
43
+ },
44
+
45
+ observe: true,
46
+ },
47
+
48
+ hasMany: ['Post'],
49
+ belongsTo: ['User'],
50
+
51
+ attributes: {
52
+ name: {
53
+ order: 1,
54
+ fillable: true,
55
+ validation: {
56
+ rule: schema.string().required().min(5).max(255),
57
+ message: {
58
+ min: 'Name must have a minimum of 3 characters',
59
+ max: 'Name must have a maximum of 255 characters',
60
+ },
61
+ },
62
+
63
+ factory: faker => faker.person.fullName(),
64
+ },
65
+
66
+ email: {
67
+ unique: true,
68
+ order: 2,
69
+ fillable: true,
70
+ validation: {
71
+ rule: schema.string().required().email(),
72
+ message: {
73
+ email: 'Email must be a valid email address',
74
+ },
75
+ },
76
+
77
+ factory: faker => faker.internet.email(),
78
+ },
79
+
80
+ bio: {
81
+ required: false,
82
+ order: 3,
83
+ fillable: true,
84
+ validation: {
85
+ rule: schema.string().max(500),
86
+ message: {
87
+ max: 'Bio must have a maximum of 500 characters',
88
+ },
89
+ },
90
+ factory: faker => faker.lorem.sentence(),
91
+ },
92
+
93
+ avatar: {
94
+ required: false,
95
+ order: 4,
96
+ fillable: true,
97
+ validation: {
98
+ rule: schema.string().url(),
99
+ message: {
100
+ url: 'Avatar must be a valid URL',
101
+ },
102
+ },
103
+ factory: faker => faker.image.avatar(),
104
+ },
105
+ },
106
+ dashboard: {
107
+ highlight: true,
108
+ },
109
+ } as const)
@@ -0,0 +1,56 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * A named navigation slot on a site - `main`, `footer`, `portal`. The items
6
+ * are MenuItem rows; templates fetch the tree by handle
7
+ * (`fetchMenuTree(siteId, 'main')`) so navigation is content, not code.
8
+ */
9
+ export default defineModel({
10
+ name: 'Menu',
11
+ table: 'menus',
12
+ primaryKey: 'id',
13
+ autoIncrement: true,
14
+
15
+ indexes: [
16
+ {
17
+ name: 'menus_site_handle_unique',
18
+ columns: ['site_id', 'handle'],
19
+ unique: true,
20
+ },
21
+ ],
22
+
23
+ traits: {
24
+ useTimestamps: true,
25
+ useApi: {
26
+ middleware: ['auth'],
27
+ uri: 'menus',
28
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
29
+ },
30
+ },
31
+
32
+ belongsTo: ['Site'],
33
+ hasMany: ['MenuItem'],
34
+
35
+ attributes: {
36
+ handle: {
37
+ required: true,
38
+ order: 1,
39
+ fillable: true,
40
+ validation: {
41
+ rule: schema.string().min(2).max(64).matches(/^[a-z0-9-]+$/),
42
+ },
43
+ factory: faker => faker.helpers.arrayElement(['main', 'footer', 'portal']),
44
+ },
45
+
46
+ name: {
47
+ required: true,
48
+ order: 2,
49
+ fillable: true,
50
+ validation: {
51
+ rule: schema.string().min(2).max(255),
52
+ },
53
+ factory: faker => faker.lorem.words(2),
54
+ },
55
+ },
56
+ } as const)
@@ -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)
@@ -0,0 +1,205 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
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
+ */
17
+ export default defineModel({
18
+ name: 'Page',
19
+ table: 'pages',
20
+ primaryKey: 'id',
21
+ autoIncrement: true,
22
+
23
+ indexes: [
24
+ {
25
+ name: 'pages_site_path_unique',
26
+ columns: ['site_id', 'path'],
27
+ unique: true,
28
+ },
29
+ ],
30
+
31
+ traits: {
32
+ useUuid: true,
33
+ useTimestamps: true,
34
+ useSearch: {
35
+ displayable: ['id', 'title', 'slug', 'path', 'template', 'status', 'views'],
36
+ searchable: ['title', 'slug', 'path'],
37
+ sortable: ['views', 'conversions', 'publishedAt'],
38
+ filterable: ['template', 'status'],
39
+ },
40
+ useApi: {
41
+ middleware: ['auth'],
42
+ uri: 'pages',
43
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
44
+ },
45
+
46
+ useSeeder: {
47
+ count: 10,
48
+ },
49
+ },
50
+
51
+ belongsTo: ['Author', 'Site'],
52
+
53
+ attributes: {
54
+ title: {
55
+ required: true,
56
+ order: 1,
57
+ fillable: true,
58
+ validation: {
59
+ rule: schema.string().min(3).max(255),
60
+ message: {
61
+ min: 'Title must have a minimum of 3 characters',
62
+ max: 'Title must have a maximum of 255 characters',
63
+ },
64
+ },
65
+ factory: faker => faker.lorem.sentence(),
66
+ },
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
+
105
+ template: {
106
+ required: true,
107
+ order: 5,
108
+ fillable: true,
109
+ validation: {
110
+ rule: schema.string().min(3),
111
+ message: {
112
+ min: 'Template must have a minimum of 3 characters',
113
+ },
114
+ },
115
+ factory: faker => faker.helpers.arrayElement(['default', 'landing', 'blog', 'contact']),
116
+ },
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
+
161
+ views: {
162
+ required: false,
163
+ order: 10,
164
+ fillable: true,
165
+ default: 0,
166
+ validation: {
167
+ rule: schema.number().min(0),
168
+ message: {
169
+ min: 'Views count cannot be negative',
170
+ },
171
+ },
172
+ factory: faker => faker.number.int({ min: 0, max: 1000 }),
173
+ },
174
+
175
+ publishedAt: {
176
+ required: false,
177
+ order: 11,
178
+ fillable: true,
179
+ validation: {
180
+ rule: schema.timestamp(),
181
+ message: {
182
+ timestamp: 'Published timestamp must be a valid timestamp',
183
+ },
184
+ },
185
+ factory: (faker) => {
186
+ const date = faker.date.past()
187
+ return date.toISOString().slice(0, 19).replace('T', ' ')
188
+ },
189
+ },
190
+
191
+ conversions: {
192
+ required: false,
193
+ order: 12,
194
+ fillable: true,
195
+ default: 0,
196
+ validation: {
197
+ rule: schema.number().min(0),
198
+ message: {
199
+ min: 'Conversions count cannot be negative',
200
+ },
201
+ },
202
+ factory: faker => faker.number.int({ min: 0, max: 100 }),
203
+ },
204
+ },
205
+ } as const)