@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.
- package/dist/index.d.ts +97 -97
- package/dist/index.js +7 -7
- package/dist/models/Activity.ts +95 -0
- package/dist/models/AnalyticsEvent.ts +70 -0
- package/dist/models/Automation.ts +23 -0
- package/dist/models/AutomationRun.ts +26 -0
- package/dist/models/Board.ts +104 -0
- package/dist/models/BoardColumn.ts +88 -0
- package/dist/models/Campaign.ts +321 -0
- package/dist/models/CampaignSend.ts +191 -0
- package/dist/models/CampaignVariant.ts +25 -0
- package/dist/models/Card.ts +118 -0
- package/dist/models/CardComment.ts +68 -0
- package/dist/models/Comment.ts +109 -0
- package/dist/models/CommunicationSuppression.ts +22 -0
- package/dist/models/ConsentEvent.ts +30 -0
- package/dist/models/Content/Author.ts +109 -0
- package/dist/models/Content/Menu.ts +56 -0
- package/dist/models/Content/MenuItem.ts +91 -0
- package/dist/models/Content/Page.ts +205 -0
- package/dist/models/Content/PageRevision.ts +86 -0
- package/dist/models/Content/Post.ts +230 -0
- package/dist/models/Content/Redirect.ts +80 -0
- package/dist/models/Deployment.ts +117 -0
- package/dist/models/EmailIdempotency.ts +64 -0
- package/dist/models/EmailList.ts +134 -0
- package/dist/models/EmailListSubscriber.ts +91 -0
- package/dist/models/EmailSuppression.ts +52 -0
- package/dist/models/EmailWebhookEvent.ts +51 -0
- package/dist/models/Error.ts +77 -0
- package/dist/models/FailedJob.ts +84 -0
- package/dist/models/Forms/Form.ts +91 -0
- package/dist/models/Forms/FormField.ts +127 -0
- package/dist/models/Forms/FormSubmission.ts +114 -0
- package/dist/models/Job.ts +69 -0
- package/dist/models/Label.ts +67 -0
- package/dist/models/Log.ts +134 -0
- package/dist/models/MagicLinkToken.ts +97 -0
- package/dist/models/MailPreference.ts +213 -0
- package/dist/models/Notification.ts +68 -0
- package/dist/models/NotificationDelivery.ts +102 -0
- package/dist/models/PaymentMethod.ts +98 -0
- package/dist/models/PaymentProduct.ts +91 -0
- package/dist/models/PaymentTransaction.ts +78 -0
- package/dist/models/QueryLog.ts +182 -0
- package/dist/models/README.md +31 -0
- package/dist/models/Release.ts +90 -0
- package/dist/models/Request.ts +133 -0
- package/dist/models/SenderDomain.ts +22 -0
- package/dist/models/Site.ts +112 -0
- package/dist/models/SiteDomain.ts +74 -0
- package/dist/models/SmsOptOut.ts +65 -0
- package/dist/models/SocialAccount.ts +70 -0
- package/dist/models/SocialPost.ts +144 -0
- package/dist/models/Subscriber.ts +89 -0
- package/dist/models/SubscriberEmail.ts +55 -0
- package/dist/models/Subscription.ts +153 -0
- package/dist/models/Tag.ts +105 -0
- package/dist/models/Team.ts +75 -0
- package/dist/models/TeamInvitation.ts +159 -0
- package/dist/models/TeamMember.ts +88 -0
- package/dist/models/UsageEvent.ts +24 -0
- package/dist/models/User.ts +142 -0
- package/dist/models/commerce/Auction.ts +173 -0
- package/dist/models/commerce/AuctionItem.ts +204 -0
- package/dist/models/commerce/Bid.ts +129 -0
- package/dist/models/commerce/Cart.ts +140 -0
- package/dist/models/commerce/CartItem.ts +138 -0
- package/dist/models/commerce/Category.ts +125 -0
- package/dist/models/commerce/Coupon.ts +186 -0
- package/dist/models/commerce/Customer.ts +132 -0
- package/dist/models/commerce/DeliveryRoute.ts +127 -0
- package/dist/models/commerce/DeliveryStop.ts +166 -0
- package/dist/models/commerce/DigitalDelivery.ts +106 -0
- package/dist/models/commerce/Driver.ts +168 -0
- package/dist/models/commerce/DriverPing.ts +103 -0
- package/dist/models/commerce/GiftCard.ts +195 -0
- package/dist/models/commerce/LicenseKey.ts +89 -0
- package/dist/models/commerce/LoyaltyPoint.ts +108 -0
- package/dist/models/commerce/LoyaltyReward.ts +129 -0
- package/dist/models/commerce/Manufacturer.ts +90 -0
- package/dist/models/commerce/Order.ts +198 -0
- package/dist/models/commerce/OrderIdempotency.ts +34 -0
- package/dist/models/commerce/OrderItem.ts +51 -0
- package/dist/models/commerce/Payment.ts +182 -0
- package/dist/models/commerce/Pledge.ts +112 -0
- package/dist/models/commerce/PrintDevice.ts +120 -0
- package/dist/models/commerce/Product.ts +153 -0
- package/dist/models/commerce/ProductUnit.ts +113 -0
- package/dist/models/commerce/ProductVariant.ts +111 -0
- package/dist/models/commerce/Receipt.ts +144 -0
- package/dist/models/commerce/Review.ts +171 -0
- package/dist/models/commerce/ShippingMethod.ts +102 -0
- package/dist/models/commerce/ShippingRate.ts +79 -0
- package/dist/models/commerce/ShippingZone.ts +116 -0
- package/dist/models/commerce/TaxRate.ts +167 -0
- package/dist/models/commerce/Transaction.ts +127 -0
- package/dist/models/commerce/WaitlistProduct.ts +151 -0
- package/dist/models/commerce/WaitlistRestaurant.ts +189 -0
- package/dist/models/realtime/Websocket.ts +78 -0
- package/dist/password-policy.ts +46 -0
- 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)
|