@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,321 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'Campaign',
6
+ table: 'campaigns',
7
+ primaryKey: 'id',
8
+ autoIncrement: true,
9
+ belongsTo: ['Team', 'EmailList'],
10
+ hasMany: ['CampaignSend', 'CampaignVariant'],
11
+
12
+ traits: {
13
+ useUuid: true,
14
+ useTimestamps: true,
15
+ useSeeder: {
16
+ count: 10,
17
+ },
18
+ useApi: {
19
+ uri: 'campaigns',
20
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
21
+ middleware: ['auth', 'team'],
22
+ },
23
+ useSearch: {
24
+ displayable: ['id', 'name', 'type', 'status', 'subject', 'scheduledAt', 'sentAt'],
25
+ searchable: ['name', 'description', 'subject'],
26
+ sortable: ['name', 'type', 'status', 'scheduledAt', 'sentAt', 'createdAt', 'updatedAt'],
27
+ filterable: ['teamId', 'type', 'status', 'emailListId', 'currency'],
28
+ },
29
+ observe: true,
30
+ },
31
+
32
+ attributes: {
33
+ name: {
34
+ required: true,
35
+ fillable: true,
36
+ validation: {
37
+ rule: schema.string().min(3).max(255),
38
+ },
39
+ factory: faker => faker.helpers.arrayElement([
40
+ 'Summer Sale 2024', 'Black Friday Deals', 'Holiday Special',
41
+ 'New Product Launch', 'Customer Appreciation', 'Flash Sale',
42
+ 'Welcome Series', 'Re-engagement', 'Newsletter Weekly',
43
+ 'Product Updates',
44
+ ]),
45
+ },
46
+
47
+ description: {
48
+ required: false,
49
+ fillable: true,
50
+ validation: {
51
+ rule: schema.string().max(1000),
52
+ },
53
+ factory: faker => faker.lorem.paragraph(),
54
+ },
55
+
56
+ type: {
57
+ required: true,
58
+ fillable: true,
59
+ validation: {
60
+ rule: schema.enum(['email', 'sms', 'push', 'social', 'multi-channel']),
61
+ },
62
+ factory: faker => faker.helpers.arrayElement(['email', 'sms', 'push', 'social', 'multi-channel']),
63
+ },
64
+
65
+ status: {
66
+ required: true,
67
+ fillable: true,
68
+ default: 'draft',
69
+ // Newsletter sends use 'sending' / 'sent' / 'cancelled' / 'failed'.
70
+ // The legacy multi-channel statuses (active / completed / archived)
71
+ // are kept for compatibility with non-email campaign types.
72
+ validation: {
73
+ rule: schema.enum([
74
+ 'draft', 'scheduled', 'sending', 'sent', 'paused', 'cancelled', 'failed',
75
+ 'active', 'completed', 'archived',
76
+ ]),
77
+ },
78
+ factory: faker => faker.helpers.arrayElement(['draft', 'scheduled', 'sent', 'sending', 'completed']),
79
+ },
80
+
81
+ // ── Email-specific fields (used when type === 'email') ────────────
82
+ // The newsletter pipeline writes these. Other campaign types
83
+ // (sms/push/social) leave them null.
84
+
85
+ subject: {
86
+ required: false,
87
+ fillable: true,
88
+ validation: {
89
+ rule: schema.string().max(255),
90
+ },
91
+ factory: faker => faker.lorem.sentence(6),
92
+ },
93
+
94
+ template: {
95
+ required: false,
96
+ fillable: true,
97
+ validation: {
98
+ rule: schema.string(),
99
+ },
100
+ factory: faker => faker.helpers.arrayElement(['newsletter-default', 'product-update', 'promo']),
101
+ },
102
+
103
+ text: {
104
+ required: false,
105
+ fillable: true,
106
+ validation: {
107
+ rule: schema.string(),
108
+ },
109
+ factory: faker => faker.lorem.paragraphs(2),
110
+ },
111
+
112
+ content: {
113
+ required: false,
114
+ fillable: true,
115
+ validation: {
116
+ rule: schema.json(),
117
+ },
118
+ factory: () => JSON.stringify([]),
119
+ },
120
+
121
+ channelSettings: {
122
+ required: false,
123
+ fillable: true,
124
+ validation: {
125
+ rule: schema.json(),
126
+ },
127
+ factory: () => JSON.stringify({}),
128
+ },
129
+
130
+ segmentDefinition: {
131
+ required: false,
132
+ fillable: true,
133
+ validation: {
134
+ rule: schema.json(),
135
+ },
136
+ factory: () => JSON.stringify({ operator: 'and', rules: [] }),
137
+ },
138
+
139
+ fromName: {
140
+ required: false,
141
+ fillable: true,
142
+ validation: {
143
+ rule: schema.string().max(100),
144
+ },
145
+ factory: faker => faker.company.name(),
146
+ },
147
+
148
+ fromAddress: {
149
+ required: false,
150
+ fillable: true,
151
+ validation: {
152
+ rule: schema.string().email().max(255),
153
+ },
154
+ factory: faker => faker.internet.email(),
155
+ },
156
+
157
+ replyTo: {
158
+ required: false,
159
+ fillable: true,
160
+ validation: {
161
+ rule: schema.string().email().max(255),
162
+ },
163
+ factory: faker => faker.internet.email(),
164
+ },
165
+
166
+ timezone: {
167
+ required: true,
168
+ fillable: true,
169
+ default: 'UTC',
170
+ validation: {
171
+ rule: schema.string().max(100),
172
+ },
173
+ factory: () => 'UTC',
174
+ },
175
+
176
+ recurrence: {
177
+ required: false,
178
+ fillable: true,
179
+ validation: {
180
+ rule: schema.string().max(255),
181
+ },
182
+ factory: () => null,
183
+ },
184
+
185
+ experimentMetric: {
186
+ required: false,
187
+ fillable: true,
188
+ validation: {
189
+ rule: schema.enum(['open_rate', 'click_rate', 'conversion_rate']),
190
+ },
191
+ factory: () => null,
192
+ },
193
+
194
+ emailListId: {
195
+ required: false,
196
+ fillable: true,
197
+ validation: {
198
+ rule: schema.number(),
199
+ },
200
+ factory: faker => faker.number.int({ min: 1, max: 8 }),
201
+ },
202
+
203
+ scheduledAt: {
204
+ required: false,
205
+ fillable: true,
206
+ validation: {
207
+ rule: schema.timestamp(),
208
+ },
209
+ factory: (faker) => {
210
+ return faker.date.soon({ days: 14 }).toISOString().slice(0, 19).replace('T', ' ')
211
+ },
212
+ },
213
+
214
+ sentAt: {
215
+ required: false,
216
+ fillable: true,
217
+ validation: {
218
+ rule: schema.timestamp(),
219
+ },
220
+ factory: () => null,
221
+ },
222
+
223
+ audienceSize: {
224
+ required: false,
225
+ fillable: true,
226
+ default: 0,
227
+ validation: {
228
+ rule: schema.number().min(0),
229
+ },
230
+ factory: faker => faker.number.int({ min: 100, max: 50000 }),
231
+ },
232
+
233
+ sentCount: {
234
+ required: false,
235
+ fillable: true,
236
+ default: 0,
237
+ validation: {
238
+ rule: schema.number().min(0),
239
+ },
240
+ factory: faker => faker.number.int({ min: 0, max: 10000 }),
241
+ },
242
+
243
+ openRate: {
244
+ required: false,
245
+ fillable: true,
246
+ validation: {
247
+ rule: schema.number().min(0).max(100),
248
+ },
249
+ factory: faker => faker.number.float({ min: 15, max: 45, fractionDigits: 1 }),
250
+ },
251
+
252
+ clickRate: {
253
+ required: false,
254
+ fillable: true,
255
+ validation: {
256
+ rule: schema.number().min(0).max(100),
257
+ },
258
+ factory: faker => faker.number.float({ min: 2, max: 15, fractionDigits: 1 }),
259
+ },
260
+
261
+ conversionRate: {
262
+ required: false,
263
+ fillable: true,
264
+ validation: {
265
+ rule: schema.number().min(0).max(100),
266
+ },
267
+ factory: faker => faker.number.float({ min: 0.5, max: 8, fractionDigits: 1 }),
268
+ },
269
+
270
+ budget: {
271
+ required: false,
272
+ fillable: true,
273
+ validation: {
274
+ rule: schema.number().min(0),
275
+ },
276
+ factory: faker => faker.number.float({ min: 100, max: 10000, fractionDigits: 2 }),
277
+ },
278
+
279
+ spent: {
280
+ required: false,
281
+ fillable: true,
282
+ default: 0,
283
+ validation: {
284
+ rule: schema.number().min(0),
285
+ },
286
+ factory: faker => faker.number.float({ min: 0, max: 5000, fractionDigits: 2 }),
287
+ },
288
+
289
+ currency: {
290
+ required: true,
291
+ fillable: true,
292
+ default: 'USD',
293
+ validation: {
294
+ rule: schema.string().required().max(3),
295
+ },
296
+ factory: faker => faker.helpers.arrayElement(['USD', 'EUR', 'GBP', 'CAD', 'AUD']),
297
+ },
298
+
299
+ startDate: {
300
+ required: false,
301
+ fillable: true,
302
+ validation: {
303
+ rule: schema.timestamp(),
304
+ },
305
+ factory: (faker) => {
306
+ return faker.date.recent({ days: 30 }).toISOString().slice(0, 19).replace('T', ' ')
307
+ },
308
+ },
309
+
310
+ endDate: {
311
+ required: false,
312
+ fillable: true,
313
+ validation: {
314
+ rule: schema.timestamp(),
315
+ },
316
+ factory: (faker) => {
317
+ return faker.date.soon({ days: 30 }).toISOString().slice(0, 19).replace('T', ' ')
318
+ },
319
+ },
320
+ },
321
+ } as const)
@@ -0,0 +1,191 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'CampaignSend',
6
+ table: 'campaign_sends',
7
+ primaryKey: 'id',
8
+ autoIncrement: true,
9
+ belongsTo: ['Team', 'Campaign', 'Subscriber', 'EmailList', 'CampaignVariant'],
10
+
11
+ indexes: [
12
+ {
13
+ name: 'campaign_sends_idempotency_unique',
14
+ columns: ['idempotency_key'],
15
+ unique: true,
16
+ },
17
+ ],
18
+
19
+ traits: {
20
+ useUuid: true,
21
+ useTimestamps: true,
22
+ useApi: {
23
+ uri: 'campaign-sends',
24
+ routes: ['index', 'show'],
25
+ // Per-recipient send records carry email + delivery status. Treat
26
+ // as PII and require auth on all read paths. The transactional
27
+ // owner-only views in the dashboard are gated separately.
28
+ middleware: ['auth', 'team'],
29
+ },
30
+ },
31
+
32
+ attributes: {
33
+ campaignId: {
34
+ required: true,
35
+ fillable: true,
36
+ validation: {
37
+ rule: schema.number(),
38
+ },
39
+ factory: faker => faker.number.int({ min: 1, max: 10 }),
40
+ },
41
+
42
+ subscriberId: {
43
+ required: false,
44
+ fillable: true,
45
+ validation: {
46
+ rule: schema.number(),
47
+ },
48
+ factory: () => null,
49
+ },
50
+
51
+ emailListId: {
52
+ required: false,
53
+ fillable: true,
54
+ validation: {
55
+ rule: schema.number(),
56
+ },
57
+ factory: () => null,
58
+ },
59
+
60
+ status: {
61
+ required: true,
62
+ fillable: true,
63
+ default: 'queued',
64
+ validation: {
65
+ rule: schema.enum([
66
+ 'queued', 'deferred', 'sending', 'sent', 'delivered', 'failed',
67
+ 'undelivered', 'bounced', 'complained', 'suppressed', 'cancelled',
68
+ ]),
69
+ },
70
+ factory: faker => faker.helpers.arrayElement(['sent', 'sent', 'sent', 'queued', 'failed', 'bounced']),
71
+ },
72
+
73
+ channel: {
74
+ required: true,
75
+ fillable: true,
76
+ default: 'email',
77
+ validation: {
78
+ rule: schema.enum(['email', 'sms', 'push']),
79
+ },
80
+ factory: faker => faker.helpers.arrayElement(['email', 'sms']),
81
+ },
82
+
83
+ recipient: {
84
+ required: true,
85
+ fillable: true,
86
+ validation: {
87
+ rule: schema.string().max(255),
88
+ },
89
+ factory: faker => faker.internet.email(),
90
+ },
91
+
92
+ idempotencyKey: {
93
+ required: true,
94
+ fillable: true,
95
+ validation: {
96
+ rule: schema.string().max(255),
97
+ },
98
+ factory: faker => faker.string.uuid(),
99
+ },
100
+
101
+ providerMessageId: {
102
+ required: false,
103
+ fillable: true,
104
+ validation: {
105
+ rule: schema.string().max(255),
106
+ },
107
+ factory: faker => faker.string.uuid(),
108
+ },
109
+
110
+ error: {
111
+ required: false,
112
+ fillable: true,
113
+ validation: {
114
+ rule: schema.string(),
115
+ },
116
+ factory: () => null,
117
+ },
118
+
119
+ sentAt: {
120
+ required: false,
121
+ fillable: true,
122
+ validation: {
123
+ rule: schema.timestamp(),
124
+ },
125
+ factory: faker => faker.date.recent({ days: 7 }).toISOString().slice(0, 19).replace('T', ' '),
126
+ },
127
+
128
+ openedAt: {
129
+ required: false,
130
+ fillable: true,
131
+ validation: {
132
+ rule: schema.timestamp(),
133
+ },
134
+ factory: () => null,
135
+ },
136
+
137
+ clickedAt: {
138
+ required: false,
139
+ fillable: true,
140
+ validation: {
141
+ rule: schema.timestamp(),
142
+ },
143
+ factory: () => null,
144
+ },
145
+
146
+ deliveredAt: {
147
+ required: false,
148
+ fillable: true,
149
+ validation: {
150
+ rule: schema.timestamp(),
151
+ },
152
+ factory: () => null,
153
+ },
154
+
155
+ failedAt: {
156
+ required: false,
157
+ fillable: true,
158
+ validation: {
159
+ rule: schema.timestamp(),
160
+ },
161
+ factory: () => null,
162
+ },
163
+
164
+ segments: {
165
+ required: false,
166
+ fillable: true,
167
+ validation: {
168
+ rule: schema.number().min(1),
169
+ },
170
+ factory: () => 1,
171
+ },
172
+
173
+ cost: {
174
+ required: false,
175
+ fillable: true,
176
+ validation: {
177
+ rule: schema.number().min(0),
178
+ },
179
+ factory: () => 0,
180
+ },
181
+
182
+ metadata: {
183
+ required: false,
184
+ fillable: true,
185
+ validation: {
186
+ rule: schema.json(),
187
+ },
188
+ factory: () => JSON.stringify({}),
189
+ },
190
+ },
191
+ } as const)
@@ -0,0 +1,25 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'CampaignVariant',
6
+ table: 'campaign_variants',
7
+ belongsTo: ['Team', 'Campaign'],
8
+ traits: {
9
+ useUuid: true,
10
+ useTimestamps: true,
11
+ useApi: { uri: 'campaign-variants', routes: ['index', 'store', 'show', 'update', 'destroy'], middleware: ['auth', 'team'] },
12
+ },
13
+ indexes: [{ name: 'campaign_variants_name_unique', columns: ['campaign_id', 'name'], unique: true }],
14
+ attributes: {
15
+ name: { required: true, fillable: true, validation: { rule: schema.string().max(80) }, factory: faker => faker.helpers.arrayElement(['Control', 'Variant B']) },
16
+ subject: { required: false, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.lorem.sentence(6) },
17
+ content: { required: true, fillable: true, validation: { rule: schema.json() }, factory: () => JSON.stringify([]) },
18
+ allocation: { required: true, fillable: true, default: 50, validation: { rule: schema.number().min(0).max(100) }, factory: () => 50 },
19
+ sentCount: { required: true, fillable: true, default: 0, validation: { rule: schema.number().min(0) }, factory: () => 0 },
20
+ openCount: { required: true, fillable: true, default: 0, validation: { rule: schema.number().min(0) }, factory: () => 0 },
21
+ clickCount: { required: true, fillable: true, default: 0, validation: { rule: schema.number().min(0) }, factory: () => 0 },
22
+ conversionCount: { required: true, fillable: true, default: 0, validation: { rule: schema.number().min(0) }, factory: () => 0 },
23
+ isWinner: { required: true, fillable: true, default: false, validation: { rule: schema.boolean() }, factory: () => false },
24
+ },
25
+ } as const)
@@ -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)