@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,88 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A user's membership in one team.
|
|
6
|
+
*
|
|
7
|
+
* The auth package already resolves active-team context from this table.
|
|
8
|
+
* Keeping the pivot as a model gives applications a typed, model-driven
|
|
9
|
+
* schema and an authenticated REST surface instead of relying on an
|
|
10
|
+
* undocumented table that may not exist.
|
|
11
|
+
*/
|
|
12
|
+
export default defineModel({
|
|
13
|
+
name: 'TeamMember',
|
|
14
|
+
table: 'team_members',
|
|
15
|
+
primaryKey: 'id',
|
|
16
|
+
autoIncrement: true,
|
|
17
|
+
|
|
18
|
+
indexes: [
|
|
19
|
+
{
|
|
20
|
+
name: 'team_members_team_user_unique',
|
|
21
|
+
columns: ['team_id', 'user_id'],
|
|
22
|
+
unique: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'team_members_user_status_index',
|
|
26
|
+
columns: ['user_id', 'status'],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
belongsTo: ['Team', 'User'],
|
|
31
|
+
|
|
32
|
+
traits: {
|
|
33
|
+
useUuid: true,
|
|
34
|
+
useTimestamps: true,
|
|
35
|
+
useSeeder: { count: 0 },
|
|
36
|
+
useSearch: {
|
|
37
|
+
displayable: ['id', 'teamId', 'userId', 'role', 'status', 'createdAt'],
|
|
38
|
+
searchable: [],
|
|
39
|
+
sortable: ['role', 'status', 'createdAt', 'updatedAt'],
|
|
40
|
+
filterable: ['teamId', 'userId', 'role', 'status'],
|
|
41
|
+
},
|
|
42
|
+
useApi: {
|
|
43
|
+
uri: 'team-members',
|
|
44
|
+
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
45
|
+
middleware: ['auth', 'team'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
attributes: {
|
|
50
|
+
teamId: {
|
|
51
|
+
required: true,
|
|
52
|
+
fillable: true,
|
|
53
|
+
validation: {
|
|
54
|
+
rule: schema.number().required(),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
userId: {
|
|
59
|
+
required: true,
|
|
60
|
+
fillable: true,
|
|
61
|
+
validation: {
|
|
62
|
+
rule: schema.number().required(),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
role: {
|
|
67
|
+
required: true,
|
|
68
|
+
fillable: true,
|
|
69
|
+
default: 'member',
|
|
70
|
+
validation: {
|
|
71
|
+
rule: schema.enum(['owner', 'admin', 'member', 'viewer']),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
status: {
|
|
76
|
+
required: true,
|
|
77
|
+
fillable: true,
|
|
78
|
+
default: 'active',
|
|
79
|
+
validation: {
|
|
80
|
+
rule: schema.enum(['active', 'suspended']),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
dashboard: {
|
|
86
|
+
enabled: false,
|
|
87
|
+
},
|
|
88
|
+
} as const)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
export default defineModel({
|
|
5
|
+
name: 'UsageEvent',
|
|
6
|
+
table: 'usage_events',
|
|
7
|
+
belongsTo: ['Team'],
|
|
8
|
+
traits: {
|
|
9
|
+
useUuid: true,
|
|
10
|
+
useTimestamps: true,
|
|
11
|
+
useApi: { uri: 'usage-events', routes: ['index', 'show'], middleware: ['auth', 'team'] },
|
|
12
|
+
},
|
|
13
|
+
indexes: [
|
|
14
|
+
{ name: 'usage_events_idempotency_unique', columns: ['idempotency_key'], unique: true },
|
|
15
|
+
{ name: 'usage_events_meter_period', columns: ['team_id', 'meter', 'occurred_at'] },
|
|
16
|
+
],
|
|
17
|
+
attributes: {
|
|
18
|
+
meter: { required: true, fillable: true, validation: { rule: schema.enum(['contacts', 'email_sends', 'sms_segments', 'ai_generations', 'storage_bytes']) }, factory: () => 'email_sends' },
|
|
19
|
+
quantity: { required: true, fillable: true, default: 1, validation: { rule: schema.number().min(0) }, factory: () => 1 },
|
|
20
|
+
idempotencyKey: { required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.string.uuid() },
|
|
21
|
+
metadata: { required: false, fillable: true, validation: { rule: schema.json() }, factory: () => JSON.stringify({}) },
|
|
22
|
+
occurredAt: { required: true, fillable: true, validation: { rule: schema.timestamp() }, factory: () => new Date().toISOString() },
|
|
23
|
+
},
|
|
24
|
+
} as const)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Attributes } from '@stacksjs/types'
|
|
2
|
+
import { defineModel } from '@stacksjs/orm'
|
|
3
|
+
import { makeHash } from '@stacksjs/security'
|
|
4
|
+
// soon, these will be auto-imported
|
|
5
|
+
import { schema } from '@stacksjs/validation'
|
|
6
|
+
import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from '../password-policy'
|
|
7
|
+
|
|
8
|
+
export default defineModel({
|
|
9
|
+
name: 'User', // defaults to the sanitized file name
|
|
10
|
+
table: 'users', // defaults to the lowercase, plural name of the model name (or the name of the model file)
|
|
11
|
+
primaryKey: 'id', // defaults to `id`
|
|
12
|
+
autoIncrement: true, // defaults to true
|
|
13
|
+
|
|
14
|
+
// Define composite indexes
|
|
15
|
+
indexes: [
|
|
16
|
+
{
|
|
17
|
+
name: 'users_email_name_index',
|
|
18
|
+
columns: ['email', 'name'],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
traits: {
|
|
23
|
+
useAuth: {
|
|
24
|
+
usePasskey: true,
|
|
25
|
+
},
|
|
26
|
+
// Not every app bills through the User model — leave `billable`
|
|
27
|
+
// off by default here. An app that wants checkout()/
|
|
28
|
+
// createStripeUser()/activeSubscription() etc. (createBillableMethods
|
|
29
|
+
// in orm/define-model.ts) should `buddy publish:model User` and
|
|
30
|
+
// enable the trait on its own app/Models/User.ts override, alongside
|
|
31
|
+
// the matching users.stripe_id guarantee-ALTER already provided by
|
|
32
|
+
// ensureUsersAuthColumns() regardless of this flag.
|
|
33
|
+
billable: false,
|
|
34
|
+
useUuid: true,
|
|
35
|
+
useTimestamps: true, // defaults to true, `timestampable` used as an alias
|
|
36
|
+
useSocials: ['github'],
|
|
37
|
+
useSearch: {
|
|
38
|
+
displayable: ['id', 'name', 'email'], // the fields to become d (defaults to all fields)
|
|
39
|
+
searchable: ['name', 'email'], // the fields to become searchable (defaults to all fields)
|
|
40
|
+
sortable: ['created_at', 'updated_at'], // the fields to become sortable (defaults to all fields)
|
|
41
|
+
filterable: [], // the fields to become filterable (defaults to all fields)
|
|
42
|
+
// options: {}, // you may pass options to the search engine
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
useSeeder: {
|
|
46
|
+
// defaults to a count of 10, `seedable` used as an alias
|
|
47
|
+
count: 10,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
useApi: {
|
|
51
|
+
uri: 'users', // your-url.com/api/users
|
|
52
|
+
|
|
53
|
+
routes: ['index', 'store', 'show'],
|
|
54
|
+
// Registration is handled by the dedicated, rate-limited
|
|
55
|
+
// `/register` action. The generated User CRUD surface exposes PII
|
|
56
|
+
// and must only be available to authenticated operators.
|
|
57
|
+
middleware: ['auth'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
hasOne: ['Subscriber', 'Driver', 'Author'],
|
|
62
|
+
|
|
63
|
+
hasMany: [
|
|
64
|
+
'PersonalAccessToken',
|
|
65
|
+
'Customer',
|
|
66
|
+
'TeamMember',
|
|
67
|
+
],
|
|
68
|
+
attributes: {
|
|
69
|
+
name: {
|
|
70
|
+
order: 2,
|
|
71
|
+
fillable: true,
|
|
72
|
+
validation: {
|
|
73
|
+
rule: schema.string().required().min(5).max(100),
|
|
74
|
+
message: {
|
|
75
|
+
min: 'Name must have a minimum of 3 characters',
|
|
76
|
+
max: 'Name must have a maximum of 255 characters',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
factory: faker => faker.person.fullName(),
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
email: {
|
|
84
|
+
unique: true,
|
|
85
|
+
order: 1,
|
|
86
|
+
fillable: true,
|
|
87
|
+
validation: {
|
|
88
|
+
rule: schema.string().email().required(),
|
|
89
|
+
message: {
|
|
90
|
+
required: 'Email is required',
|
|
91
|
+
email: 'Email must be a valid email address',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
factory: faker => faker.internet.email(),
|
|
96
|
+
},
|
|
97
|
+
password: {
|
|
98
|
+
order: 3,
|
|
99
|
+
hidden: true,
|
|
100
|
+
fillable: true,
|
|
101
|
+
validation: {
|
|
102
|
+
rule: schema.string().required().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),
|
|
103
|
+
message: {
|
|
104
|
+
required: 'Password is required',
|
|
105
|
+
min: `Password must have a minimum of ${PASSWORD_MIN_LENGTH} characters`,
|
|
106
|
+
max: `Password must have a maximum of ${PASSWORD_MAX_LENGTH} characters`,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Must satisfy the rule above, or seeded users fail their own model's
|
|
111
|
+
// validation. `123456` did once the minimum moved to 8 (#2226).
|
|
112
|
+
factory: () => 'password1234',
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
avatar: {
|
|
116
|
+
order: 4,
|
|
117
|
+
fillable: true,
|
|
118
|
+
validation: {
|
|
119
|
+
rule: schema.string().max(2048),
|
|
120
|
+
message: {
|
|
121
|
+
max: 'Avatar URL must be at most 2048 characters',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
factory: faker => faker.image.avatar(),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
get: {
|
|
129
|
+
salutationName: (attributes: Attributes) => {
|
|
130
|
+
return `Mr. ${attributes.name}`
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
set: {
|
|
135
|
+
password: async (attributes: Attributes) => {
|
|
136
|
+
return await makeHash(attributes.password, { algorithm: 'bcrypt' })
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
dashboard: {
|
|
140
|
+
highlight: true,
|
|
141
|
+
},
|
|
142
|
+
} as const)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An auction: a catalogue of lots that opens, takes bids, and closes.
|
|
6
|
+
*
|
|
7
|
+
* `eventId` is a plain column rather than a `belongsTo`, because the thing an
|
|
8
|
+
* auction hangs off is the application's own event model - a gala, a fun run,
|
|
9
|
+
* a capital campaign evening - and the framework does not ship one. An app that
|
|
10
|
+
* has an Event model declares `hasOne: ['Auction']` on its side and the column
|
|
11
|
+
* below is what that resolves through; an app with no events leaves it null and
|
|
12
|
+
* runs the auction standalone.
|
|
13
|
+
*/
|
|
14
|
+
export default defineModel({
|
|
15
|
+
name: 'Auction',
|
|
16
|
+
table: 'auctions',
|
|
17
|
+
primaryKey: 'id',
|
|
18
|
+
autoIncrement: true,
|
|
19
|
+
|
|
20
|
+
traits: {
|
|
21
|
+
useUuid: true,
|
|
22
|
+
useTimestamps: true,
|
|
23
|
+
|
|
24
|
+
useSearch: {
|
|
25
|
+
displayable: ['id', 'title', 'status', 'opensAt', 'closesAt', 'goalAmount'],
|
|
26
|
+
searchable: ['title', 'description'],
|
|
27
|
+
sortable: ['createdAt', 'opensAt', 'closesAt'],
|
|
28
|
+
filterable: ['status'],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
useApi: {
|
|
32
|
+
uri: 'auctions',
|
|
33
|
+
middleware: { read: ['auth'], write: ['auth'] },
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
observe: true,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
hasMany: ['AuctionItem', 'Bid', 'Pledge'],
|
|
40
|
+
|
|
41
|
+
indexes: [
|
|
42
|
+
{ name: 'auctions_event_id_index', columns: ['event_id'] },
|
|
43
|
+
{ name: 'auctions_status_index', columns: ['status'] },
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
attributes: {
|
|
47
|
+
eventId: {
|
|
48
|
+
order: 1,
|
|
49
|
+
fillable: true,
|
|
50
|
+
validation: {
|
|
51
|
+
rule: schema.number(),
|
|
52
|
+
},
|
|
53
|
+
factory: faker => faker.number.int({ min: 1, max: 20 }),
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
title: {
|
|
57
|
+
order: 2,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.string().required().max(160),
|
|
61
|
+
message: {
|
|
62
|
+
required: 'Title is required',
|
|
63
|
+
max: 'Title must have a maximum of 160 characters',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
factory: () => 'Spring Benefit Auction',
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
description: {
|
|
70
|
+
order: 3,
|
|
71
|
+
fillable: true,
|
|
72
|
+
validation: {
|
|
73
|
+
rule: schema.string(),
|
|
74
|
+
},
|
|
75
|
+
factory: faker => faker.lorem.sentence(),
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
status: {
|
|
79
|
+
default: 'draft',
|
|
80
|
+
order: 4,
|
|
81
|
+
fillable: true,
|
|
82
|
+
validation: {
|
|
83
|
+
rule: schema.enum(['draft', 'preview', 'open', 'closed', 'settled']).required(),
|
|
84
|
+
message: {
|
|
85
|
+
required: 'Status is required',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
factory: () => 'draft',
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
currency: {
|
|
92
|
+
default: 'USD',
|
|
93
|
+
order: 5,
|
|
94
|
+
fillable: true,
|
|
95
|
+
validation: {
|
|
96
|
+
rule: schema.string().max(3),
|
|
97
|
+
},
|
|
98
|
+
factory: () => 'USD',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The fundraising target, in cents. Null when the school would rather not
|
|
103
|
+
* put a number on the wall.
|
|
104
|
+
*/
|
|
105
|
+
goalAmount: {
|
|
106
|
+
order: 6,
|
|
107
|
+
fillable: true,
|
|
108
|
+
validation: {
|
|
109
|
+
rule: schema.number().min(0),
|
|
110
|
+
},
|
|
111
|
+
factory: faker => faker.number.int({ min: 500000, max: 5000000 }),
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
opensAt: {
|
|
115
|
+
order: 7,
|
|
116
|
+
fillable: true,
|
|
117
|
+
validation: {
|
|
118
|
+
rule: schema.date().required(),
|
|
119
|
+
},
|
|
120
|
+
factory: faker => faker.date.soon().toISOString(),
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
closesAt: {
|
|
124
|
+
order: 8,
|
|
125
|
+
fillable: true,
|
|
126
|
+
validation: {
|
|
127
|
+
rule: schema.date().required(),
|
|
128
|
+
},
|
|
129
|
+
factory: faker => faker.date.future().toISOString(),
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** How far a lot's close is pushed out when a bid lands in the window. */
|
|
133
|
+
antiSnipeMinutes: {
|
|
134
|
+
default: 2,
|
|
135
|
+
order: 9,
|
|
136
|
+
fillable: true,
|
|
137
|
+
validation: {
|
|
138
|
+
rule: schema.number().min(0).max(60),
|
|
139
|
+
},
|
|
140
|
+
factory: () => 2,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/** How close to the end a bid has to land to trigger an extension. */
|
|
144
|
+
extendOnBidWindowMinutes: {
|
|
145
|
+
default: 2,
|
|
146
|
+
order: 10,
|
|
147
|
+
fillable: true,
|
|
148
|
+
validation: {
|
|
149
|
+
rule: schema.number().min(0).max(60),
|
|
150
|
+
},
|
|
151
|
+
factory: () => 2,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The hard stop. Without it two determined bidders can hold one lot open
|
|
156
|
+
* all night and the gala staff cannot go home.
|
|
157
|
+
*/
|
|
158
|
+
maxExtensions: {
|
|
159
|
+
default: 20,
|
|
160
|
+
order: 11,
|
|
161
|
+
fillable: true,
|
|
162
|
+
validation: {
|
|
163
|
+
rule: schema.number().min(0),
|
|
164
|
+
},
|
|
165
|
+
factory: () => 20,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
dashboard: {
|
|
170
|
+
icon: 'i-hugeicons-auction',
|
|
171
|
+
label: 'Auctions',
|
|
172
|
+
},
|
|
173
|
+
} as const)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One lot in an auction.
|
|
6
|
+
*
|
|
7
|
+
* Every money column is integer cents. A benefit auction adds several hundred
|
|
8
|
+
* bids together and reports the total to a board; floats lose that argument.
|
|
9
|
+
*/
|
|
10
|
+
export default defineModel({
|
|
11
|
+
name: 'AuctionItem',
|
|
12
|
+
table: 'auction_items',
|
|
13
|
+
primaryKey: 'id',
|
|
14
|
+
autoIncrement: true,
|
|
15
|
+
|
|
16
|
+
traits: {
|
|
17
|
+
useUuid: true,
|
|
18
|
+
useTimestamps: true,
|
|
19
|
+
|
|
20
|
+
useSearch: {
|
|
21
|
+
displayable: ['id', 'lotNumber', 'title', 'status', 'startingBid', 'donorName'],
|
|
22
|
+
searchable: ['title', 'description', 'donorName', 'category'],
|
|
23
|
+
sortable: ['lotNumber', 'startingBid', 'closesAt'],
|
|
24
|
+
filterable: ['status', 'category'],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
useApi: {
|
|
28
|
+
uri: 'auction-items',
|
|
29
|
+
middleware: { read: ['auth'], write: ['auth'] },
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
observe: true,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
belongsTo: ['Auction'],
|
|
36
|
+
hasMany: ['Bid'],
|
|
37
|
+
|
|
38
|
+
indexes: [
|
|
39
|
+
{ name: 'auction_items_auction_id_index', columns: ['auction_id'] },
|
|
40
|
+
{ name: 'auction_items_status_index', columns: ['status'] },
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
attributes: {
|
|
44
|
+
lotNumber: {
|
|
45
|
+
order: 1,
|
|
46
|
+
fillable: true,
|
|
47
|
+
validation: {
|
|
48
|
+
rule: schema.number().required().min(1),
|
|
49
|
+
message: {
|
|
50
|
+
required: 'Lot number is required',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
factory: faker => faker.number.int({ min: 1, max: 120 }),
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
title: {
|
|
57
|
+
order: 2,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.string().required().max(160),
|
|
61
|
+
message: {
|
|
62
|
+
required: 'Title is required',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
factory: faker => faker.commerce.productName(),
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
description: {
|
|
69
|
+
order: 3,
|
|
70
|
+
fillable: true,
|
|
71
|
+
validation: {
|
|
72
|
+
rule: schema.string(),
|
|
73
|
+
},
|
|
74
|
+
factory: faker => faker.lorem.paragraph(),
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
imageUrl: {
|
|
78
|
+
order: 4,
|
|
79
|
+
fillable: true,
|
|
80
|
+
validation: {
|
|
81
|
+
rule: schema.string().max(500),
|
|
82
|
+
},
|
|
83
|
+
factory: () => '',
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
category: {
|
|
87
|
+
order: 5,
|
|
88
|
+
fillable: true,
|
|
89
|
+
validation: {
|
|
90
|
+
rule: schema.string().max(60),
|
|
91
|
+
},
|
|
92
|
+
factory: faker => faker.helpers.arrayElement(['experiences', 'travel', 'dining', 'sports', 'art', 'services']),
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/** Who donated the lot. It goes on the tax letter and the thank-you note. */
|
|
96
|
+
donorName: {
|
|
97
|
+
order: 6,
|
|
98
|
+
fillable: true,
|
|
99
|
+
validation: {
|
|
100
|
+
rule: schema.string().max(160),
|
|
101
|
+
},
|
|
102
|
+
factory: faker => faker.company.name(),
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What the lot is worth, in cents. Drives the sell-through report and the
|
|
107
|
+
* "paid over value" number the development office cares about.
|
|
108
|
+
*/
|
|
109
|
+
fairMarketValue: {
|
|
110
|
+
order: 7,
|
|
111
|
+
fillable: true,
|
|
112
|
+
validation: {
|
|
113
|
+
rule: schema.number().min(0),
|
|
114
|
+
},
|
|
115
|
+
factory: faker => faker.number.int({ min: 5000, max: 200000 }),
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
startingBid: {
|
|
119
|
+
order: 8,
|
|
120
|
+
fillable: true,
|
|
121
|
+
validation: {
|
|
122
|
+
rule: schema.number().required().min(0),
|
|
123
|
+
message: {
|
|
124
|
+
required: 'Starting bid is required',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
factory: faker => faker.number.int({ min: 2500, max: 50000 }),
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A fixed increment for this lot, in cents. Null - the normal case - means
|
|
132
|
+
* the auction-wide ladder applies, which steps bigger as the money gets
|
|
133
|
+
* bigger.
|
|
134
|
+
*/
|
|
135
|
+
minIncrement: {
|
|
136
|
+
order: 9,
|
|
137
|
+
fillable: true,
|
|
138
|
+
validation: {
|
|
139
|
+
rule: schema.number().min(0),
|
|
140
|
+
},
|
|
141
|
+
factory: () => null,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/** Bid at or above this and the lot sells immediately. */
|
|
145
|
+
buyNowPrice: {
|
|
146
|
+
order: 10,
|
|
147
|
+
fillable: true,
|
|
148
|
+
validation: {
|
|
149
|
+
rule: schema.number().min(0),
|
|
150
|
+
},
|
|
151
|
+
factory: () => null,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** Below this the lot passes rather than selling. */
|
|
155
|
+
reservePrice: {
|
|
156
|
+
order: 11,
|
|
157
|
+
fillable: true,
|
|
158
|
+
validation: {
|
|
159
|
+
rule: schema.number().min(0),
|
|
160
|
+
},
|
|
161
|
+
factory: () => null,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
status: {
|
|
165
|
+
default: 'open',
|
|
166
|
+
order: 12,
|
|
167
|
+
fillable: true,
|
|
168
|
+
validation: {
|
|
169
|
+
rule: schema.enum(['open', 'closed', 'sold', 'passed']).required(),
|
|
170
|
+
},
|
|
171
|
+
factory: () => 'open',
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A per-lot close, so a gala can stagger its sections and the room is not
|
|
176
|
+
* asked to watch a hundred lots end at once. Null falls back to the
|
|
177
|
+
* auction's own close. Anti-snipe extensions are written here.
|
|
178
|
+
*/
|
|
179
|
+
closesAt: {
|
|
180
|
+
order: 13,
|
|
181
|
+
fillable: true,
|
|
182
|
+
validation: {
|
|
183
|
+
rule: schema.date(),
|
|
184
|
+
},
|
|
185
|
+
factory: () => null,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** How many times anti-snipe has already extended this lot. */
|
|
189
|
+
extensionCount: {
|
|
190
|
+
default: 0,
|
|
191
|
+
order: 14,
|
|
192
|
+
fillable: false,
|
|
193
|
+
validation: {
|
|
194
|
+
rule: schema.number().min(0),
|
|
195
|
+
},
|
|
196
|
+
factory: () => 0,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
dashboard: {
|
|
201
|
+
icon: 'i-hugeicons-package',
|
|
202
|
+
label: 'Lots',
|
|
203
|
+
},
|
|
204
|
+
} as const)
|