@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.
- package/dist/index.d.ts +97 -97
- 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,166 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One address on a delivery route.
|
|
6
|
+
*
|
|
7
|
+
* `DeliveryRoute` records that a route happened and how far it went; this is
|
|
8
|
+
* the route itself. Without it `stops` is an integer, which is enough to bill
|
|
9
|
+
* a driver and not nearly enough to tell a customer where their order is: the
|
|
10
|
+
* link from an order to the vehicle carrying it runs through here.
|
|
11
|
+
*
|
|
12
|
+
* Sequence is the planned order. `status` is what actually happened, which
|
|
13
|
+
* diverges the moment a driver skips a building and comes back to it.
|
|
14
|
+
*/
|
|
15
|
+
export default defineModel({
|
|
16
|
+
name: 'DeliveryStop',
|
|
17
|
+
table: 'delivery_stops',
|
|
18
|
+
primaryKey: 'id',
|
|
19
|
+
autoIncrement: true,
|
|
20
|
+
|
|
21
|
+
traits: {
|
|
22
|
+
useUuid: true,
|
|
23
|
+
useTimestamps: true,
|
|
24
|
+
|
|
25
|
+
useSearch: {
|
|
26
|
+
displayable: ['id', 'sequence', 'status', 'address', 'etaAt'],
|
|
27
|
+
searchable: ['address', 'recipientName'],
|
|
28
|
+
sortable: ['sequence', 'etaAt', 'createdAt'],
|
|
29
|
+
filterable: ['status', 'deliveryRouteId'],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
useSeeder: { count: 0 },
|
|
33
|
+
|
|
34
|
+
useApi: {
|
|
35
|
+
// A stop names a customer and their address. Staff only, both ways.
|
|
36
|
+
// Customers reach their own stop through the order tracking route,
|
|
37
|
+
// which authorises on the order's tracking token instead.
|
|
38
|
+
middleware: ['auth'],
|
|
39
|
+
uri: 'delivery-stops',
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
observe: true,
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
belongsTo: ['DeliveryRoute', 'Order'],
|
|
46
|
+
|
|
47
|
+
attributes: {
|
|
48
|
+
/** Position in the planned run, 1-based. */
|
|
49
|
+
sequence: {
|
|
50
|
+
order: 1,
|
|
51
|
+
required: true,
|
|
52
|
+
fillable: true,
|
|
53
|
+
default: 1,
|
|
54
|
+
validation: {
|
|
55
|
+
rule: schema.number().required().min(1),
|
|
56
|
+
message: { min: 'Sequence starts at 1' },
|
|
57
|
+
},
|
|
58
|
+
factory: faker => faker.number.int({ min: 1, max: 20 }),
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
status: {
|
|
62
|
+
order: 2,
|
|
63
|
+
required: true,
|
|
64
|
+
fillable: true,
|
|
65
|
+
default: 'pending',
|
|
66
|
+
validation: {
|
|
67
|
+
rule: schema.enum(['pending', 'en_route', 'arrived', 'completed', 'failed', 'skipped']),
|
|
68
|
+
message: {
|
|
69
|
+
enum: 'Status must be one of: pending, en_route, arrived, completed, failed, skipped',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
factory: faker => faker.helpers.arrayElement(['pending', 'en_route', 'completed']),
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
address: {
|
|
76
|
+
order: 3,
|
|
77
|
+
required: true,
|
|
78
|
+
fillable: true,
|
|
79
|
+
validation: { rule: schema.string().required().max(255) },
|
|
80
|
+
factory: faker => faker.location.streetAddress(true),
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Geocoded destination. Nullable because an address can arrive before it
|
|
85
|
+
* resolves, and a stop with no coordinates is still a stop the driver can
|
|
86
|
+
* complete; it just cannot be drawn.
|
|
87
|
+
*/
|
|
88
|
+
latitude: {
|
|
89
|
+
order: 4,
|
|
90
|
+
fillable: true,
|
|
91
|
+
validation: { rule: schema.number().min(-90).max(90) },
|
|
92
|
+
factory: faker => faker.location.latitude(),
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
longitude: {
|
|
96
|
+
order: 5,
|
|
97
|
+
fillable: true,
|
|
98
|
+
validation: { rule: schema.number().min(-180).max(180) },
|
|
99
|
+
factory: faker => faker.location.longitude(),
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
recipientName: {
|
|
103
|
+
order: 6,
|
|
104
|
+
fillable: true,
|
|
105
|
+
validation: { rule: schema.string().max(255) },
|
|
106
|
+
factory: faker => faker.person.fullName(),
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
recipientPhone: {
|
|
110
|
+
order: 7,
|
|
111
|
+
fillable: true,
|
|
112
|
+
validation: { rule: schema.string().max(40) },
|
|
113
|
+
factory: faker => faker.phone.number(),
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/** Current estimate, rewritten as the driver moves. */
|
|
117
|
+
etaAt: {
|
|
118
|
+
order: 8,
|
|
119
|
+
fillable: true,
|
|
120
|
+
validation: { rule: schema.timestamp() },
|
|
121
|
+
factory: () => new Date(Date.now() + 25 * 60_000).toISOString(),
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* When the customer was told the driver was close.
|
|
126
|
+
*
|
|
127
|
+
* The latch that makes "nearly there" fire once. Without it every ping
|
|
128
|
+
* inside the radius sends another text, which is roughly one text every
|
|
129
|
+
* four seconds for the last four hundred metres.
|
|
130
|
+
*/
|
|
131
|
+
notifiedNearbyAt: {
|
|
132
|
+
order: 9,
|
|
133
|
+
fillable: true,
|
|
134
|
+
validation: { rule: schema.timestamp() },
|
|
135
|
+
factory: () => null,
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/** When the driver crossed the arrival radius. */
|
|
139
|
+
arrivedAt: {
|
|
140
|
+
order: 10,
|
|
141
|
+
fillable: true,
|
|
142
|
+
validation: { rule: schema.timestamp() },
|
|
143
|
+
factory: () => null,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/** When the handover finished. Together with `arrivedAt`, dwell time. */
|
|
147
|
+
completedAt: {
|
|
148
|
+
order: 11,
|
|
149
|
+
fillable: true,
|
|
150
|
+
validation: { rule: schema.timestamp() },
|
|
151
|
+
factory: () => null,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** Why a stop failed, or anything the driver needs to record. */
|
|
155
|
+
notes: {
|
|
156
|
+
order: 12,
|
|
157
|
+
fillable: true,
|
|
158
|
+
validation: { rule: schema.string().max(1000) },
|
|
159
|
+
factory: () => '',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
dashboard: {
|
|
164
|
+
highlight: true,
|
|
165
|
+
},
|
|
166
|
+
} as const)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
export default defineModel({
|
|
5
|
+
name: 'DigitalDelivery',
|
|
6
|
+
table: 'digital_deliveries',
|
|
7
|
+
primaryKey: 'id',
|
|
8
|
+
autoIncrement: true,
|
|
9
|
+
|
|
10
|
+
traits: {
|
|
11
|
+
useUuid: true,
|
|
12
|
+
useTimestamps: true,
|
|
13
|
+
useSearch: {
|
|
14
|
+
displayable: ['id', 'name', 'description', 'downloadLimit', 'expiryDays', 'status'],
|
|
15
|
+
searchable: ['name', 'description'],
|
|
16
|
+
sortable: ['name', 'downloadLimit', 'expiryDays', 'status', 'createdAt', 'updatedAt'],
|
|
17
|
+
filterable: ['status'],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
useSeeder: {
|
|
21
|
+
count: 10,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
useApi: {
|
|
25
|
+
uri: 'digital-deliveries',
|
|
26
|
+
middleware: ['auth'],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
observe: true,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
attributes: {
|
|
33
|
+
name: {
|
|
34
|
+
order: 2,
|
|
35
|
+
fillable: true,
|
|
36
|
+
validation: {
|
|
37
|
+
rule: schema.string().required().max(255),
|
|
38
|
+
message: {
|
|
39
|
+
max: 'Name must have a maximum of 255 characters',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
factory: faker => faker.commerce.productName(),
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
description: {
|
|
46
|
+
order: 3,
|
|
47
|
+
fillable: true,
|
|
48
|
+
validation: {
|
|
49
|
+
rule: schema.string().required(),
|
|
50
|
+
},
|
|
51
|
+
factory: faker => faker.commerce.productDescription(),
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
downloadLimit: {
|
|
55
|
+
order: 4,
|
|
56
|
+
fillable: true,
|
|
57
|
+
validation: {
|
|
58
|
+
rule: schema.number().min(0),
|
|
59
|
+
},
|
|
60
|
+
factory: faker => faker.number.int({ min: 1, max: 100 }),
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
expiryDays: {
|
|
64
|
+
order: 5,
|
|
65
|
+
fillable: true,
|
|
66
|
+
validation: {
|
|
67
|
+
rule: schema.number().required().min(0),
|
|
68
|
+
},
|
|
69
|
+
factory: faker => faker.number.int({ min: 1, max: 365 }),
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
requiresLogin: {
|
|
73
|
+
default: false,
|
|
74
|
+
order: 6,
|
|
75
|
+
fillable: true,
|
|
76
|
+
validation: {
|
|
77
|
+
rule: schema.boolean(),
|
|
78
|
+
},
|
|
79
|
+
factory: faker => faker.datatype.boolean(),
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
automaticDelivery: {
|
|
83
|
+
default: false,
|
|
84
|
+
order: 7,
|
|
85
|
+
fillable: true,
|
|
86
|
+
validation: {
|
|
87
|
+
rule: schema.boolean(),
|
|
88
|
+
},
|
|
89
|
+
factory: faker => faker.datatype.boolean(),
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
status: {
|
|
93
|
+
default: 'active',
|
|
94
|
+
order: 8,
|
|
95
|
+
fillable: true,
|
|
96
|
+
validation: {
|
|
97
|
+
rule: schema.enum(['active', 'inactive']),
|
|
98
|
+
},
|
|
99
|
+
factory: faker => faker.helpers.arrayElement(['active', 'inactive']),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
dashboard: {
|
|
104
|
+
highlight: true,
|
|
105
|
+
},
|
|
106
|
+
} as const)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
export default defineModel({
|
|
5
|
+
name: 'Driver',
|
|
6
|
+
table: 'drivers',
|
|
7
|
+
primaryKey: 'id',
|
|
8
|
+
autoIncrement: true,
|
|
9
|
+
|
|
10
|
+
belongsTo: ['User'],
|
|
11
|
+
hasMany: ['DeliveryRoute', 'DriverPing'],
|
|
12
|
+
|
|
13
|
+
traits: {
|
|
14
|
+
useUuid: true,
|
|
15
|
+
useTimestamps: true,
|
|
16
|
+
useSearch: {
|
|
17
|
+
displayable: ['id', 'name', 'phone', 'vehicleNumber', 'license', 'status'],
|
|
18
|
+
searchable: ['name', 'phone', 'vehicleNumber', 'license'],
|
|
19
|
+
sortable: ['name', 'status', 'createdAt', 'updatedAt'],
|
|
20
|
+
filterable: ['status'],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
useSeeder: {
|
|
24
|
+
count: 10,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
useApi: {
|
|
28
|
+
uri: 'drivers',
|
|
29
|
+
middleware: ['auth'],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
observe: true,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
attributes: {
|
|
36
|
+
name: {
|
|
37
|
+
order: 1,
|
|
38
|
+
fillable: true,
|
|
39
|
+
validation: {
|
|
40
|
+
rule: schema.string().required().max(255),
|
|
41
|
+
message: {
|
|
42
|
+
max: 'Name must have a maximum of 255 characters',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
factory: faker => faker.person.fullName(),
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
phone: {
|
|
49
|
+
order: 2,
|
|
50
|
+
fillable: true,
|
|
51
|
+
validation: {
|
|
52
|
+
rule: schema.string().required().max(255),
|
|
53
|
+
message: {
|
|
54
|
+
max: 'Phone number must have a maximum of 255 characters',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
factory: faker => faker.phone.number(),
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
vehicleNumber: {
|
|
61
|
+
order: 3,
|
|
62
|
+
fillable: true,
|
|
63
|
+
validation: {
|
|
64
|
+
rule: schema.string().required().max(255),
|
|
65
|
+
message: {
|
|
66
|
+
max: 'Vehicle number must have a maximum of 255 characters',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
factory: faker => faker.vehicle.vrm(),
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
license: {
|
|
73
|
+
order: 4,
|
|
74
|
+
fillable: true,
|
|
75
|
+
validation: {
|
|
76
|
+
rule: schema.string().required().max(255),
|
|
77
|
+
message: {
|
|
78
|
+
max: 'License number must have a maximum of 255 characters',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
factory: faker => faker.string.alphanumeric({ length: 10, casing: 'upper' }),
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
status: {
|
|
85
|
+
default: 'active',
|
|
86
|
+
order: 5,
|
|
87
|
+
fillable: true,
|
|
88
|
+
validation: {
|
|
89
|
+
rule: schema.enum(['active', 'on_delivery', 'on_break', 'offline']),
|
|
90
|
+
},
|
|
91
|
+
factory: faker => faker.helpers.arrayElement(['active', 'on_delivery', 'on_break']),
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* Last known position.
|
|
96
|
+
*
|
|
97
|
+
* Denormalised from the `driver_pings` series on purpose: "where is this
|
|
98
|
+
* driver right now" is asked on every map frame and by every tracking
|
|
99
|
+
* page, and answering it with a MAX(recorded_at) subquery over a table
|
|
100
|
+
* that grows by a row every few seconds is the wrong shape. The series is
|
|
101
|
+
* the history; these three columns are the present.
|
|
102
|
+
*/
|
|
103
|
+
latitude: {
|
|
104
|
+
order: 6,
|
|
105
|
+
fillable: true,
|
|
106
|
+
validation: {
|
|
107
|
+
rule: schema.number().min(-90).max(90),
|
|
108
|
+
message: {
|
|
109
|
+
min: 'Latitude must be between -90 and 90',
|
|
110
|
+
max: 'Latitude must be between -90 and 90',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
factory: faker => faker.location.latitude(),
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
longitude: {
|
|
117
|
+
order: 7,
|
|
118
|
+
fillable: true,
|
|
119
|
+
validation: {
|
|
120
|
+
rule: schema.number().min(-180).max(180),
|
|
121
|
+
message: {
|
|
122
|
+
min: 'Longitude must be between -180 and 180',
|
|
123
|
+
max: 'Longitude must be between -180 and 180',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
factory: faker => faker.location.longitude(),
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/** Degrees clockwise from true north, so a map can rotate the marker. */
|
|
130
|
+
heading: {
|
|
131
|
+
order: 8,
|
|
132
|
+
fillable: true,
|
|
133
|
+
validation: {
|
|
134
|
+
rule: schema.number().min(0).max(360),
|
|
135
|
+
},
|
|
136
|
+
factory: faker => faker.number.int({ min: 0, max: 359 }),
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/** Metres per second, as reported by the device. */
|
|
140
|
+
speed: {
|
|
141
|
+
order: 9,
|
|
142
|
+
fillable: true,
|
|
143
|
+
default: 0,
|
|
144
|
+
validation: {
|
|
145
|
+
rule: schema.number().min(0),
|
|
146
|
+
},
|
|
147
|
+
factory: () => 0,
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* When the last fix landed. A tracking map needs this to say "updated 4
|
|
152
|
+
* seconds ago" and, more importantly, to stop claiming a driver is at a
|
|
153
|
+
* position that is ten minutes stale.
|
|
154
|
+
*/
|
|
155
|
+
lastPingAt: {
|
|
156
|
+
order: 10,
|
|
157
|
+
fillable: true,
|
|
158
|
+
validation: {
|
|
159
|
+
rule: schema.timestamp(),
|
|
160
|
+
},
|
|
161
|
+
factory: () => new Date().toISOString(),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
dashboard: {
|
|
166
|
+
highlight: true,
|
|
167
|
+
},
|
|
168
|
+
} as const)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One position fix from a driver's device.
|
|
6
|
+
*
|
|
7
|
+
* The append-only history behind `drivers.latitude/longitude`. A tracking map
|
|
8
|
+
* needs the present position, but everything else about a delivery needs the
|
|
9
|
+
* series: drawing the path already travelled, replaying a disputed drop,
|
|
10
|
+
* measuring how long a stop actually took, and deriving an ETA from recent
|
|
11
|
+
* speed rather than straight-line distance.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately not `useSearch` (indexing a row every few seconds is pointless
|
|
14
|
+
* and expensive) and not `useApi` (writes come through
|
|
15
|
+
* `recordDriverPing`, which does the fan-out, and reads are scoped to a
|
|
16
|
+
* route). `usePrunable` keeps the table from growing without bound.
|
|
17
|
+
*/
|
|
18
|
+
export default defineModel({
|
|
19
|
+
name: 'DriverPing',
|
|
20
|
+
table: 'driver_pings',
|
|
21
|
+
primaryKey: 'id',
|
|
22
|
+
autoIncrement: true,
|
|
23
|
+
|
|
24
|
+
traits: {
|
|
25
|
+
useUuid: true,
|
|
26
|
+
useTimestamps: true,
|
|
27
|
+
useSeeder: { count: 0 },
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
belongsTo: ['Driver', 'DeliveryRoute'],
|
|
31
|
+
|
|
32
|
+
attributes: {
|
|
33
|
+
latitude: {
|
|
34
|
+
order: 1,
|
|
35
|
+
required: true,
|
|
36
|
+
fillable: true,
|
|
37
|
+
validation: {
|
|
38
|
+
rule: schema.number().required().min(-90).max(90),
|
|
39
|
+
message: {
|
|
40
|
+
min: 'Latitude must be between -90 and 90',
|
|
41
|
+
max: 'Latitude must be between -90 and 90',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
factory: faker => faker.location.latitude(),
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
longitude: {
|
|
48
|
+
order: 2,
|
|
49
|
+
required: true,
|
|
50
|
+
fillable: true,
|
|
51
|
+
validation: {
|
|
52
|
+
rule: schema.number().required().min(-180).max(180),
|
|
53
|
+
message: {
|
|
54
|
+
min: 'Longitude must be between -180 and 180',
|
|
55
|
+
max: 'Longitude must be between -180 and 180',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
factory: faker => faker.location.longitude(),
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/** Degrees clockwise from true north. */
|
|
62
|
+
heading: {
|
|
63
|
+
order: 3,
|
|
64
|
+
fillable: true,
|
|
65
|
+
validation: { rule: schema.number().min(0).max(360) },
|
|
66
|
+
factory: faker => faker.number.int({ min: 0, max: 359 }),
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/** Metres per second. */
|
|
70
|
+
speed: {
|
|
71
|
+
order: 4,
|
|
72
|
+
fillable: true,
|
|
73
|
+
default: 0,
|
|
74
|
+
validation: { rule: schema.number().min(0) },
|
|
75
|
+
factory: faker => faker.number.int({ min: 0, max: 30 }),
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reported horizontal accuracy in metres. Worth keeping: a fix with 800m
|
|
80
|
+
* of error should move the marker differently than one with 5m, and
|
|
81
|
+
* without this the map cannot tell them apart.
|
|
82
|
+
*/
|
|
83
|
+
accuracy: {
|
|
84
|
+
order: 5,
|
|
85
|
+
fillable: true,
|
|
86
|
+
validation: { rule: schema.number().min(0) },
|
|
87
|
+
factory: faker => faker.number.int({ min: 3, max: 60 }),
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* When the device took the fix, which is not when the server received it.
|
|
92
|
+
* A driver through a tunnel sends five fixes at once on the far side, and
|
|
93
|
+
* ordering them by arrival draws a path that never happened.
|
|
94
|
+
*/
|
|
95
|
+
recordedAt: {
|
|
96
|
+
order: 6,
|
|
97
|
+
required: true,
|
|
98
|
+
fillable: true,
|
|
99
|
+
validation: { rule: schema.timestamp().required() },
|
|
100
|
+
factory: () => new Date().toISOString(),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
} as const)
|