@stacksjs/orm 0.72.16 → 0.72.17

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 (101) hide show
  1. package/dist/index.d.ts +97 -97
  2. package/dist/models/Activity.ts +95 -0
  3. package/dist/models/AnalyticsEvent.ts +70 -0
  4. package/dist/models/Automation.ts +23 -0
  5. package/dist/models/AutomationRun.ts +26 -0
  6. package/dist/models/Board.ts +104 -0
  7. package/dist/models/BoardColumn.ts +88 -0
  8. package/dist/models/Campaign.ts +321 -0
  9. package/dist/models/CampaignSend.ts +191 -0
  10. package/dist/models/CampaignVariant.ts +25 -0
  11. package/dist/models/Card.ts +118 -0
  12. package/dist/models/CardComment.ts +68 -0
  13. package/dist/models/Comment.ts +109 -0
  14. package/dist/models/CommunicationSuppression.ts +22 -0
  15. package/dist/models/ConsentEvent.ts +30 -0
  16. package/dist/models/Content/Author.ts +109 -0
  17. package/dist/models/Content/Menu.ts +56 -0
  18. package/dist/models/Content/MenuItem.ts +91 -0
  19. package/dist/models/Content/Page.ts +205 -0
  20. package/dist/models/Content/PageRevision.ts +86 -0
  21. package/dist/models/Content/Post.ts +230 -0
  22. package/dist/models/Content/Redirect.ts +80 -0
  23. package/dist/models/Deployment.ts +117 -0
  24. package/dist/models/EmailIdempotency.ts +64 -0
  25. package/dist/models/EmailList.ts +134 -0
  26. package/dist/models/EmailListSubscriber.ts +91 -0
  27. package/dist/models/EmailSuppression.ts +52 -0
  28. package/dist/models/EmailWebhookEvent.ts +51 -0
  29. package/dist/models/Error.ts +77 -0
  30. package/dist/models/FailedJob.ts +84 -0
  31. package/dist/models/Forms/Form.ts +91 -0
  32. package/dist/models/Forms/FormField.ts +127 -0
  33. package/dist/models/Forms/FormSubmission.ts +114 -0
  34. package/dist/models/Job.ts +69 -0
  35. package/dist/models/Label.ts +67 -0
  36. package/dist/models/Log.ts +134 -0
  37. package/dist/models/MagicLinkToken.ts +97 -0
  38. package/dist/models/MailPreference.ts +213 -0
  39. package/dist/models/Notification.ts +68 -0
  40. package/dist/models/NotificationDelivery.ts +102 -0
  41. package/dist/models/PaymentMethod.ts +98 -0
  42. package/dist/models/PaymentProduct.ts +91 -0
  43. package/dist/models/PaymentTransaction.ts +78 -0
  44. package/dist/models/QueryLog.ts +182 -0
  45. package/dist/models/README.md +31 -0
  46. package/dist/models/Release.ts +90 -0
  47. package/dist/models/Request.ts +133 -0
  48. package/dist/models/SenderDomain.ts +22 -0
  49. package/dist/models/Site.ts +112 -0
  50. package/dist/models/SiteDomain.ts +74 -0
  51. package/dist/models/SmsOptOut.ts +65 -0
  52. package/dist/models/SocialAccount.ts +70 -0
  53. package/dist/models/SocialPost.ts +144 -0
  54. package/dist/models/Subscriber.ts +89 -0
  55. package/dist/models/SubscriberEmail.ts +55 -0
  56. package/dist/models/Subscription.ts +153 -0
  57. package/dist/models/Tag.ts +105 -0
  58. package/dist/models/Team.ts +75 -0
  59. package/dist/models/TeamInvitation.ts +159 -0
  60. package/dist/models/TeamMember.ts +88 -0
  61. package/dist/models/UsageEvent.ts +24 -0
  62. package/dist/models/User.ts +142 -0
  63. package/dist/models/commerce/Auction.ts +173 -0
  64. package/dist/models/commerce/AuctionItem.ts +204 -0
  65. package/dist/models/commerce/Bid.ts +129 -0
  66. package/dist/models/commerce/Cart.ts +140 -0
  67. package/dist/models/commerce/CartItem.ts +138 -0
  68. package/dist/models/commerce/Category.ts +125 -0
  69. package/dist/models/commerce/Coupon.ts +186 -0
  70. package/dist/models/commerce/Customer.ts +132 -0
  71. package/dist/models/commerce/DeliveryRoute.ts +127 -0
  72. package/dist/models/commerce/DeliveryStop.ts +166 -0
  73. package/dist/models/commerce/DigitalDelivery.ts +106 -0
  74. package/dist/models/commerce/Driver.ts +168 -0
  75. package/dist/models/commerce/DriverPing.ts +103 -0
  76. package/dist/models/commerce/GiftCard.ts +195 -0
  77. package/dist/models/commerce/LicenseKey.ts +89 -0
  78. package/dist/models/commerce/LoyaltyPoint.ts +108 -0
  79. package/dist/models/commerce/LoyaltyReward.ts +129 -0
  80. package/dist/models/commerce/Manufacturer.ts +90 -0
  81. package/dist/models/commerce/Order.ts +198 -0
  82. package/dist/models/commerce/OrderIdempotency.ts +34 -0
  83. package/dist/models/commerce/OrderItem.ts +51 -0
  84. package/dist/models/commerce/Payment.ts +182 -0
  85. package/dist/models/commerce/Pledge.ts +112 -0
  86. package/dist/models/commerce/PrintDevice.ts +120 -0
  87. package/dist/models/commerce/Product.ts +153 -0
  88. package/dist/models/commerce/ProductUnit.ts +113 -0
  89. package/dist/models/commerce/ProductVariant.ts +111 -0
  90. package/dist/models/commerce/Receipt.ts +144 -0
  91. package/dist/models/commerce/Review.ts +171 -0
  92. package/dist/models/commerce/ShippingMethod.ts +102 -0
  93. package/dist/models/commerce/ShippingRate.ts +79 -0
  94. package/dist/models/commerce/ShippingZone.ts +116 -0
  95. package/dist/models/commerce/TaxRate.ts +167 -0
  96. package/dist/models/commerce/Transaction.ts +127 -0
  97. package/dist/models/commerce/WaitlistProduct.ts +151 -0
  98. package/dist/models/commerce/WaitlistRestaurant.ts +189 -0
  99. package/dist/models/realtime/Websocket.ts +78 -0
  100. package/dist/password-policy.ts +46 -0
  101. package/package.json +4 -4
@@ -0,0 +1,118 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * Kanban card (stacksjs/stacks#1846).
6
+ *
7
+ * A card belongs to a column (and, via denormalisation, to a board) so
8
+ * board-wide queries don't need a two-hop join. Cards carry labels +
9
+ * assignees via pivot tables (`card_labels`, `card_assignees`) — those
10
+ * relations land in Phase 3 alongside the card-detail modal.
11
+ *
12
+ * Hidden from the sidebar as a standalone model row — cards are seen
13
+ * through `/kanban/[boardId]` (Phase 2), not as a top-level model
14
+ * viewer. The dashboard's modular config from #1843 carries that
15
+ * `enabled: false` to both sidebars.
16
+ */
17
+ export default defineModel({
18
+ name: 'Card',
19
+ table: 'cards',
20
+ primaryKey: 'id',
21
+ autoIncrement: true,
22
+
23
+ traits: {
24
+ useUuid: true,
25
+ useTimestamps: true,
26
+ useSearch: {
27
+ displayable: ['id', 'title', 'columnId', 'boardId', 'position', 'dueDate'],
28
+ searchable: ['title', 'description'],
29
+ sortable: ['position', 'createdAt', 'updatedAt', 'dueDate'],
30
+ filterable: ['boardId', 'columnId', 'archived'],
31
+ },
32
+
33
+ useSeeder: { count: 0 },
34
+
35
+ useApi: {
36
+ uri: 'cards',
37
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
38
+ middleware: ['auth'],
39
+ },
40
+ },
41
+
42
+ belongsTo: ['BoardColumn', 'Board', 'User'],
43
+ // Pivot relations land in Phase 3 (labels + assignees).
44
+ // belongsToMany: {
45
+ // labels: { model: 'Label', table: 'card_labels', foreignKey: 'card_id', relatedKey: 'label_id' },
46
+ // assignees: { model: 'User', table: 'card_assignees', foreignKey: 'card_id', relatedKey: 'user_id' },
47
+ // },
48
+
49
+ attributes: {
50
+ columnId: {
51
+ order: 1,
52
+ fillable: true,
53
+ validation: { rule: schema.number().required() },
54
+ factory: () => 1,
55
+ },
56
+
57
+ boardId: {
58
+ order: 2,
59
+ fillable: true,
60
+ // Denormalised from `column.board_id` so a "all cards on board X"
61
+ // query stays single-table. The kanban API keeps it in sync on
62
+ // card create/move (Phase 2).
63
+ validation: { rule: schema.number().required() },
64
+ factory: () => 1,
65
+ },
66
+
67
+ title: {
68
+ order: 3,
69
+ fillable: true,
70
+ validation: { rule: schema.string().required().min(1).max(300) },
71
+ factory: faker => faker.lorem.sentence({ min: 3, max: 8 }),
72
+ },
73
+
74
+ description: {
75
+ order: 4,
76
+ fillable: true,
77
+ // Markdown-formatted long description. Rendered in the card detail
78
+ // modal (Phase 3); the kanban board view shows the first line only.
79
+ validation: { rule: schema.string().max(10000) },
80
+ factory: faker => faker.lorem.paragraph(),
81
+ },
82
+
83
+ position: {
84
+ order: 5,
85
+ fillable: true,
86
+ // Sort order within the parent column. Drag-reorder reflows
87
+ // adjacent rows in a single transaction (Phase 2). Moving across
88
+ // columns updates both `columnId` and `position`.
89
+ validation: { rule: schema.number().min(0) },
90
+ factory: faker => faker.number.int({ min: 0, max: 100 }),
91
+ },
92
+
93
+ createdByUserId: {
94
+ order: 6,
95
+ fillable: true,
96
+ // The user who created the card. Set server-side from
97
+ // `request.user.id` on store; surfaced in the card detail.
98
+ validation: { rule: schema.number() },
99
+ factory: () => null,
100
+ },
101
+
102
+ dueDate: {
103
+ order: 7,
104
+ fillable: true,
105
+ validation: { rule: schema.string() },
106
+ factory: () => null,
107
+ },
108
+
109
+ archived: {
110
+ order: 8,
111
+ fillable: true,
112
+ validation: { rule: schema.boolean() },
113
+ factory: () => false,
114
+ },
115
+ },
116
+
117
+ dashboard: { enabled: false },
118
+ } as const)
@@ -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)