@stacksjs/defaults 0.75.318 → 0.75.321

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 (252) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +8 -0
  2. package/ai/skills/stacks-browse/scripts/browse.ts +16 -6
  3. package/ai/skills/stacks-browse/scripts/chrome.ts +4 -0
  4. package/ai/skills/stacks-database/SKILL.md +1 -1
  5. package/ai/skills/stacks-desktop/SKILL.md +23 -0
  6. package/ai/skills/stacks-migrations/SKILL.md +2 -2
  7. package/ai/skills/stacks-native/CATALOG.md +8 -8
  8. package/app/Actions/Cms/AuthorUpdateAction.ts +4 -4
  9. package/app/Actions/Cms/CategorizableUpdateAction.ts +4 -4
  10. package/app/Actions/Cms/PostUpdateAction.ts +4 -5
  11. package/app/Actions/Cms/TaggableUpdateAction.ts +4 -2
  12. package/app/Actions/Commerce/CustomerStoreAction.ts +4 -6
  13. package/app/Actions/Commerce/CustomerUpdateAction.ts +4 -6
  14. package/app/Actions/Commerce/PaymentStoreAction.ts +5 -8
  15. package/app/Actions/Commerce/PaymentUpdateAction.ts +20 -17
  16. package/app/Actions/Dashboard/Commerce/commerce-product-records.test.ts +7 -0
  17. package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +14 -3
  18. package/app/Actions/Dashboard/Commerce/review-records.test.ts +12 -0
  19. package/app/Actions/Dashboard/Commerce/review-records.ts +10 -5
  20. package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +2 -0
  21. package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +4 -1
  22. package/app/Actions/Dashboard/Content/PageStoreAction.ts +2 -0
  23. package/app/Actions/Dashboard/Content/PostStoreAction.ts +2 -0
  24. package/app/Actions/Dashboard/Content/TagStoreAction.ts +2 -0
  25. package/app/Actions/Dashboard/Content/content-input.ts +9 -4
  26. package/app/Actions/Dashboard/Content/content-record-input.test.ts +20 -1
  27. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +1 -1
  28. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +1 -1
  29. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +5 -1
  30. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +5 -1
  31. package/app/Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction.ts +1 -1
  32. package/app/Actions/Storefront/AddToCartAction.ts +5 -2
  33. package/app/Actions/Storefront/PlaceOrderAction.ts +6 -11
  34. package/app/Actions/Storefront/SubmitReviewAction.ts +6 -5
  35. package/app/Models/Activity.ts +1 -0
  36. package/app/Models/AnalyticsEvent.ts +1 -0
  37. package/app/Models/AutomationRun.ts +1 -1
  38. package/app/Models/BoardColumn.ts +0 -1
  39. package/app/Models/Campaign.ts +1 -1
  40. package/app/Models/CampaignSend.ts +3 -3
  41. package/app/Models/Card.ts +0 -1
  42. package/app/Models/CardComment.ts +0 -2
  43. package/app/Models/Content/Author.ts +2 -0
  44. package/app/Models/Content/Categorizable.ts +1 -0
  45. package/app/Models/Content/Menu.ts +2 -0
  46. package/app/Models/Content/MenuItem.ts +2 -0
  47. package/app/Models/Content/PageRevision.ts +2 -0
  48. package/app/Models/Content/Post.ts +2 -0
  49. package/app/Models/Content/Redirect.ts +2 -0
  50. package/app/Models/EmailListSubscriber.ts +2 -2
  51. package/app/Models/Error.ts +5 -0
  52. package/app/Models/Forms/Form.ts +2 -0
  53. package/app/Models/Forms/FormField.ts +2 -0
  54. package/app/Models/Forms/FormSubmission.ts +2 -0
  55. package/app/Models/Label.ts +0 -1
  56. package/app/Models/Log.ts +3 -1
  57. package/app/Models/MagicLinkToken.ts +0 -7
  58. package/app/Models/PaymentProduct.ts +1 -0
  59. package/app/Models/PaymentTransaction.ts +1 -0
  60. package/app/Models/Release.ts +3 -0
  61. package/app/Models/Request.ts +1 -0
  62. package/app/Models/SenderDomain.ts +2 -0
  63. package/app/Models/SmsOptOut.ts +0 -7
  64. package/app/Models/Subscription.ts +2 -0
  65. package/app/Models/commerce/Auction.ts +1 -0
  66. package/app/Models/commerce/AuctionItem.ts +1 -0
  67. package/app/Models/commerce/Cart.ts +83 -2
  68. package/app/Models/commerce/Category.ts +5 -1
  69. package/app/Models/commerce/Coupon.ts +4 -1
  70. package/app/Models/commerce/DigitalDelivery.ts +1 -0
  71. package/app/Models/commerce/GiftCard.ts +1 -0
  72. package/app/Models/commerce/LoyaltyPoint.ts +1 -0
  73. package/app/Models/commerce/LoyaltyReward.ts +3 -1
  74. package/app/Models/commerce/Order.ts +4 -1
  75. package/app/Models/commerce/OrderItem.ts +1 -0
  76. package/app/Models/commerce/Payment.ts +1 -0
  77. package/app/Models/commerce/Product.ts +15 -0
  78. package/app/Models/commerce/ProductVariant.ts +2 -0
  79. package/app/Models/commerce/Receipt.ts +1 -0
  80. package/app/Models/commerce/Review.ts +3 -1
  81. package/app/Models/commerce/Transaction.ts +1 -0
  82. package/app/Models/realtime/Websocket.ts +4 -1
  83. package/functions/dashboard-form.test.ts +10 -1
  84. package/functions/dashboard-form.ts +17 -0
  85. package/ide/vscode/package.json +1 -1
  86. package/package.json +4 -4
  87. package/resources/components/Dashboard/Analytics/AnalyticsPageHeader.stx +1 -1
  88. package/resources/components/Dashboard/App/RequestDetailsDialog.stx +1 -1
  89. package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +1 -1
  90. package/resources/components/Dashboard/Auth/ForgotPassword.stx +1 -1
  91. package/resources/components/Dashboard/Auth/Login.stx +1 -1
  92. package/resources/components/Dashboard/Auth/Register.stx +1 -1
  93. package/resources/components/Dashboard/Auth/ResetPassword.stx +1 -1
  94. package/resources/components/Dashboard/Auth/TwoFactorLogin.stx +1 -1
  95. package/resources/components/Dashboard/CodeEditor.stx +1 -1
  96. package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +1 -1
  97. package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +1 -1
  98. package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +1 -1
  99. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +1 -1
  100. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +4 -13
  101. package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +1 -1
  102. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +1 -1
  103. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +1 -1
  104. package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +1 -1
  105. package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +1 -1
  106. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +1 -1
  107. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +1 -1
  108. package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +1 -1
  109. package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +1 -1
  110. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +1 -1
  111. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +1 -1
  112. package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +1 -1
  113. package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +1 -1
  114. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +1 -1
  115. package/resources/components/Dashboard/Commerce/CommerceManufacturersTable.stx +1 -1
  116. package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +1 -1
  117. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +1 -1
  118. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +1 -1
  119. package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +1 -1
  120. package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +1 -1
  121. package/resources/components/Dashboard/Commerce/CommercePosCart.stx +1 -1
  122. package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +1 -1
  123. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +1 -1
  124. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +1 -1
  125. package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +1 -1
  126. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +27 -2
  127. package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +1 -1
  128. package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +1 -1
  129. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +7 -5
  130. package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +12 -4
  131. package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +3 -3
  132. package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +1 -1
  133. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +1 -1
  134. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
  135. package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +1 -1
  136. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +1 -1
  137. package/resources/components/Dashboard/Commerce/CommerceUnitsTable.stx +1 -1
  138. package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +1 -1
  139. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +1 -1
  140. package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +1 -1
  141. package/resources/components/Dashboard/Commerce/Delivery/CourierDeleteDialog.stx +1 -1
  142. package/resources/components/Dashboard/Commerce/Delivery/CourierDialog.stx +1 -1
  143. package/resources/components/Dashboard/Commerce/Delivery/CouriersTable.stx +1 -1
  144. package/resources/components/Dashboard/Commerce/Delivery/DeliveryPagination.stx +1 -1
  145. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +1 -1
  146. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +1 -1
  147. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +1 -1
  148. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +1 -1
  149. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +1 -1
  150. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +1 -1
  151. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +1 -1
  152. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +1 -1
  153. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +1 -1
  154. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +1 -1
  155. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +1 -1
  156. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +1 -1
  157. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +1 -1
  158. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +1 -1
  159. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +1 -1
  160. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +1 -1
  161. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +1 -1
  162. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +1 -1
  163. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +1 -1
  164. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +1 -1
  165. package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +1 -1
  166. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +1 -1
  167. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +1 -1
  168. package/resources/components/Dashboard/Commerce/PrintDevicesTable.stx +1 -1
  169. package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +1 -1
  170. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +1 -1
  171. package/resources/components/Dashboard/Commerce/PrintLogsTable.stx +1 -1
  172. package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +1 -1
  173. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +1 -1
  174. package/resources/components/Dashboard/Commerce/ProductWaitlistTable.stx +1 -1
  175. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +1 -1
  176. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +1 -1
  177. package/resources/components/Dashboard/Commerce/RestaurantWaitlistTable.stx +1 -1
  178. package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +1 -1
  179. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +1 -1
  180. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +1 -1
  181. package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +1 -1
  182. package/resources/components/Dashboard/Content/FileUploadDialog.stx +1 -1
  183. package/resources/components/Dashboard/Content/PageEditorDialog.stx +1 -1
  184. package/resources/components/Dashboard/Content/PostEditorDialog.stx +1 -1
  185. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +1 -1
  186. package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +1 -1
  187. package/resources/components/Dashboard/CreateRecordModal.stx +1 -1
  188. package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +1 -1
  189. package/resources/components/Dashboard/Elements/DropdownNavigation.stx +1 -1
  190. package/resources/components/Dashboard/Email/CapturedMailList.stx +1 -1
  191. package/resources/components/Dashboard/Email/EmailSettingsList.stx +1 -1
  192. package/resources/components/Dashboard/Email/EmailSettingsNavigation.stx +1 -1
  193. package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
  194. package/resources/components/Dashboard/Email/InboxComposer.stx +1 -1
  195. package/resources/components/Dashboard/Email/InboxMessageDetail.stx +1 -1
  196. package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
  197. package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +1 -1
  198. package/resources/components/Dashboard/Infrastructure/CloudResourceButton.stx +1 -1
  199. package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +1 -1
  200. package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +1 -1
  201. package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +1 -1
  202. package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +1 -1
  203. package/resources/components/Dashboard/Jobs/JobTable.stx +1 -1
  204. package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +1 -1
  205. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +1 -1
  206. package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +1 -1
  207. package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +1 -1
  208. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +1 -1
  209. package/resources/components/Dashboard/Marketing/CampaignsTable.stx +1 -1
  210. package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +1 -1
  211. package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +1 -1
  212. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
  213. package/resources/components/Dashboard/Marketing/RecoveryCampaignDialog.stx +1 -1
  214. package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +1 -1
  215. package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +1 -1
  216. package/resources/components/Dashboard/Marketing/SocialPostsTable.stx +1 -1
  217. package/resources/components/Dashboard/MobileSidebar.stx +1 -1
  218. package/resources/components/Dashboard/Modals/BaseModal.stx +1 -1
  219. package/resources/components/Dashboard/Modals/Popups/Alert.stx +1 -1
  220. package/resources/components/Dashboard/Modals/Popups/Toast.stx +1 -1
  221. package/resources/components/Dashboard/Modals/ToastWrapper.stx +1 -1
  222. package/resources/components/Dashboard/Models/EditRecordModal.stx +1 -1
  223. package/resources/components/Dashboard/Models/ModelRecordActions.stx +1 -1
  224. package/resources/components/Dashboard/Monitoring/ErrorTable.stx +1 -1
  225. package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +1 -1
  226. package/resources/components/Dashboard/Notifications/NotificationDeliveryTable.stx +1 -1
  227. package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +1 -1
  228. package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +1 -1
  229. package/resources/components/Dashboard/Operations/RecoveryPolicyDialog.stx +1 -1
  230. package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +1 -1
  231. package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +1 -1
  232. package/resources/components/Dashboard/UI/Checkbox.stx +1 -1
  233. package/resources/components/Dashboard/UI/ConfirmDialog.stx +1 -1
  234. package/resources/components/Dashboard/UI/Drawer.stx +1 -1
  235. package/resources/components/Dashboard/UI/Dropdown.stx +1 -1
  236. package/resources/components/Dashboard/UI/Input.stx +1 -1
  237. package/resources/components/Dashboard/UI/Modal.stx +1 -1
  238. package/resources/components/Dashboard/UI/Pagination.stx +1 -1
  239. package/resources/components/Dashboard/UI/SearchFilter.stx +1 -1
  240. package/resources/components/Dashboard/UI/Select.stx +1 -1
  241. package/resources/components/Dashboard/UI/Table.stx +1 -1
  242. package/resources/components/Dashboard/UI/Tabs.stx +1 -1
  243. package/resources/components/Dashboard/UI/Textarea.stx +1 -1
  244. package/resources/components/Dashboard/UI/Toggle.stx +1 -1
  245. package/resources/components/Dashboard/UI/WindowControls.stx +1 -1
  246. package/resources/components/NativeActionSheet.stx +1 -1
  247. package/resources/components/NativeContextMenu.stx +1 -1
  248. package/resources/components/NativeHealthButton.stx +1 -1
  249. package/resources/components/NativePermissionButton.stx +1 -1
  250. package/resources/components/NativePullToRefresh.stx +1 -1
  251. package/resources/components/NativeSegmentedControl.stx +1 -1
  252. package/resources/components/NativeSheet.stx +1 -1
@@ -3,6 +3,7 @@ import {
3
3
  commerceEmail,
4
4
  commerceIdentifier,
5
5
  commerceNumber,
6
+ commerceOptionalNumber,
6
7
  commerceOptionalIdentifier,
7
8
  commerceOptionalString,
8
9
  commerceRequiredString,
@@ -17,7 +18,8 @@ export interface ReviewRecord {
17
18
  customerId: string
18
19
  customerName: string
19
20
  customerEmail: string
20
- rating: number
21
+ /** Null for a review that is words without stars. */
22
+ rating: number | null
21
23
  title: string
22
24
  content: string
23
25
  approved: boolean
@@ -73,7 +75,7 @@ export function normalizeReviewRecord(
73
75
  customerId,
74
76
  customerName: customer?.name || '',
75
77
  customerEmail: customer?.email || '',
76
- rating: commerceNumber(commerceValue(review, 'rating'), source, 'rating', {
78
+ rating: commerceOptionalNumber(commerceValue(review, 'rating'), source, 'rating', {
77
79
  min: 1,
78
80
  max: 5,
79
81
  integer: true,
@@ -135,15 +137,18 @@ export function normalizeReviewCustomerContext(customer: any): {
135
137
  }
136
138
 
137
139
  export function summarizeReviews(records: ReviewRecord[]): ReviewSummary {
138
- const totalRating = records.reduce((sum, review) => sum + review.rating, 0)
140
+ // An unrated review is a review, not a 0-star one, so the average is taken
141
+ // over the rated reviews only.
142
+ const ratings = records.flatMap(review => review.rating === null ? [] : [review.rating])
143
+ const totalRating = ratings.reduce((sum, rating) => sum + rating, 0)
139
144
  return {
140
145
  total: records.length,
141
146
  approved: records.filter(review => review.approved).length,
142
147
  pending: records.filter(review => !review.approved).length,
143
148
  featured: records.filter(review => review.featured).length,
144
149
  verified: records.filter(review => review.verifiedPurchase).length,
145
- averageRating: records.length > 0
146
- ? Math.round(totalRating / records.length * 10) / 10
150
+ averageRating: ratings.length > 0
151
+ ? Math.round(totalRating / ratings.length * 10) / 10
147
152
  : 0,
148
153
  }
149
154
  }
@@ -52,6 +52,8 @@ export default new Action({
52
52
  created_at: now,
53
53
  updated_at: now,
54
54
  } as any)
55
+ // PostgreSQL reports no insert id without RETURNING (#3004).
56
+ .returningAll()
55
57
  .executeTakeFirst()
56
58
 
57
59
  const id = insertedId(result)
@@ -53,11 +53,14 @@ export default new Action({
53
53
  name,
54
54
  slug,
55
55
  description,
56
- is_active: 1,
56
+ // A real boolean: on PostgreSQL the column is boolean and refuses 1.
57
+ is_active: true,
57
58
  display_order: Number(displayOrder?.max_display_order || 0) + 1,
58
59
  created_at: now,
59
60
  updated_at: now,
60
61
  } as any)
62
+ // PostgreSQL reports no insert id without RETURNING (#3004).
63
+ .returningAll()
61
64
  .executeTakeFirst()
62
65
 
63
66
  const id = insertedId(result)
@@ -41,6 +41,8 @@ export default new Action({
41
41
  created_at: now,
42
42
  updated_at: now,
43
43
  } as any)
44
+ // PostgreSQL reports no insert id without RETURNING (#3004).
45
+ .returningAll()
44
46
  .executeTakeFirst()
45
47
 
46
48
  const id = insertedId(result)
@@ -50,6 +50,8 @@ export default new Action({
50
50
  created_at: now,
51
51
  updated_at: now,
52
52
  } as any)
53
+ // PostgreSQL reports no insert id without RETURNING (#3004).
54
+ .returningAll()
53
55
  .executeTakeFirst()
54
56
 
55
57
  const id = insertedId(insertResult)
@@ -53,6 +53,8 @@ export default new Action({
53
53
  created_at: now,
54
54
  updated_at: now,
55
55
  } as any)
56
+ // PostgreSQL reports no insert id without RETURNING (#3004).
57
+ .returningAll()
56
58
  .executeTakeFirst()
57
59
 
58
60
  const id = insertedId(result)
@@ -20,11 +20,16 @@ export function timestamp(): string {
20
20
  return new Date().toISOString().slice(0, 19).replace('T', ' ')
21
21
  }
22
22
 
23
- /** The new row's id, across the driver shapes we may get back from an insert. */
23
+ /**
24
+ * The new row's id, across the driver shapes an insert answers with: the row
25
+ * itself from `returningAll()` (PostgreSQL, which reports no id at all without
26
+ * RETURNING, and gives a BIGSERIAL id back as a string), `lastInsertRowid`
27
+ * (SQLite) or `insertId` (MySQL). 0 when none of them names a row.
28
+ */
24
29
  export function insertedId(result: unknown): number {
25
- const row = result as { lastInsertRowid?: number | bigint, insertId?: number | bigint } | undefined
26
-
27
- return Number(row?.lastInsertRowid ?? row?.insertId ?? 0)
30
+ const answer = (Array.isArray(result) ? result[0] : result) as { id?: unknown, lastInsertRowid?: unknown, insertId?: unknown } | undefined
31
+ const id = Number(answer?.id ?? answer?.lastInsertRowid ?? answer?.insertId ?? 0)
32
+ return Number.isSafeInteger(id) && id > 0 ? id : 0
28
33
  }
29
34
 
30
35
  /** The `{id}` route param, or 0 when it is not a usable row id. */
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { describe, expect, test } from 'bun:test'
3
- import { rowId } from './content-input'
3
+ import { insertedId, rowId } from './content-input'
4
4
  import { parseAuthorInput } from './author-input'
5
5
  import { parsePageInput, parsePublished } from './page-input'
6
6
 
@@ -50,3 +50,22 @@ test('content identifiers use the shared safe-integer contract', () => {
50
50
  for (const value of [true, {}, [], Number.MAX_SAFE_INTEGER + 1]) expect(rowId({ getParam: () => value } as unknown as RequestInstance)).toBe(0)
51
51
  expect(rowId({ getParam: () => '42' } as unknown as RequestInstance)).toBe(42)
52
52
  })
53
+
54
+ // What an insert answers with on each driver (stacksjs/stacks#3004).
55
+ describe('the id of an inserted content row', () => {
56
+ test('reads the row returningAll() gives back, with PostgreSQL\'s string id', () => {
57
+ expect(insertedId({ id: '64', name: 'Example' })).toBe(64)
58
+ expect(insertedId([{ id: 7 }])).toBe(7)
59
+ })
60
+
61
+ test('reads SQLite\'s and MySQL\'s insert reports', () => {
62
+ expect(insertedId({ changes: 1, lastInsertRowid: 12 })).toBe(12)
63
+ expect(insertedId({ insertId: 9n })).toBe(9)
64
+ })
65
+
66
+ test('is 0 when nothing names a row, as PostgreSQL answers without RETURNING', () => {
67
+ expect(insertedId([])).toBe(0)
68
+ expect(insertedId(undefined)).toBe(0)
69
+ expect(insertedId({ id: 'not-an-id' })).toBe(0)
70
+ })
71
+ })
@@ -32,7 +32,7 @@ export default new Action({
32
32
  db
33
33
  .selectFrom('campaign_sends')
34
34
  .select(['campaign_id', 'status', db.fn.count('id').as('count')])
35
- .groupBy(['campaign_id', 'status'])
35
+ .groupBy('campaign_id', 'status')
36
36
  .execute(),
37
37
  db
38
38
  .selectFrom('campaign_sends')
@@ -22,7 +22,7 @@ export default new Action({
22
22
  db
23
23
  .selectFrom('email_list_subscribers')
24
24
  .select(['email_list_id', 'status', db.fn.count('id').as('count')])
25
- .groupBy(['email_list_id', 'status'])
25
+ .groupBy('email_list_id', 'status')
26
26
  .execute(),
27
27
  db
28
28
  .selectFrom('email_list_subscribers')
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions/runtime'
3
- import { toSnakeCaseKeys } from '@stacksjs/orm'
3
+ import { ModelValidationError, toSnakeCaseKeys } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { dashboardOperationalError } from '../dashboard-response'
6
6
  import { prepareModelFields, resolveWritableModel } from './model-write'
@@ -35,6 +35,10 @@ export default new Action({
35
35
  return { ok: true, id }
36
36
  }
37
37
  catch (error) {
38
+ // The model's own rules rejected the row: the caller's mistake, named
39
+ // field by field, not a server failure (stacksjs/stacks#3007).
40
+ if (error instanceof ModelValidationError)
41
+ return response.json({ message: 'Validation failed.', errors: error.errors }, 422)
38
42
  return dashboardOperationalError(error, 'The model record could not be created.', 'ModelStoreAction', 500)
39
43
  }
40
44
  },
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions/runtime'
3
- import { toSnakeCaseKeys } from '@stacksjs/orm'
3
+ import { ModelValidationError, toSnakeCaseKeys } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { dashboardOperationalError } from '../dashboard-response'
6
6
  import { parseRowId, prepareModelFields, resolveWritableModel } from './model-write'
@@ -55,6 +55,10 @@ export default new Action({
55
55
  return { ok: true, id, changed: Object.keys(changes) }
56
56
  }
57
57
  catch (error) {
58
+ // The model's own rules rejected the row: the caller's mistake, named
59
+ // field by field, not a server failure (stacksjs/stacks#3007).
60
+ if (error instanceof ModelValidationError)
61
+ return response.json({ message: 'Validation failed.', errors: error.errors }, 422)
58
62
  return dashboardOperationalError(error, 'The model record could not be updated.', 'ModelUpdateAction', 500)
59
63
  }
60
64
  },
@@ -42,7 +42,7 @@ async function notificationDeliveryOverview() {
42
42
  db
43
43
  .selectFrom('notification_deliveries')
44
44
  .select(['channel', 'status', db.fn.count('id').as('count')])
45
- .groupBy(['channel', 'status'])
45
+ .groupBy('channel', 'status')
46
46
  .execute() as unknown as Promise<ChannelStatusCountRow[]>,
47
47
  db
48
48
  .selectFrom('notification_deliveries')
@@ -1,6 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions/runtime'
2
2
  import { config } from '@stacksjs/config'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, sqlDateTime } from '@stacksjs/database/runtime'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { randomUUIDv7 } from 'bun'
6
6
  import { readCartCookie, writeCartCookie } from '../../Storefront/CartCookie'
@@ -41,7 +41,8 @@ export default new Action({
41
41
  const product = await (db as any)
42
42
  .selectFrom('products')
43
43
  .where('slug', '=', slug)
44
- .where('is_available', '=', 1)
44
+ // A boolean, not 1: PostgreSQL has no boolean = integer (#3005).
45
+ .where('is_available', '=', true)
45
46
  .selectAll()
46
47
  .executeTakeFirst()
47
48
 
@@ -70,6 +71,8 @@ export default new Action({
70
71
  currency: storefrontCurrency(config.commerce?.currency),
71
72
  session_token: token,
72
73
  checkout_step: 'cart',
74
+ // The cart lasts as long as the cookie that finds it (#3017).
75
+ expires_at: sqlDateTime(new Date(Date.now() + COOKIE_OPTS.maxAge * 1000)),
73
76
  })
74
77
  .execute()
75
78
  // Re-read by session_token: bun-query-builder's `returningAll()`
@@ -1,4 +1,6 @@
1
+ import { randomUUIDv7 } from 'bun'
1
2
  import { Action } from '@stacksjs/actions/runtime'
3
+ import { customers } from '@stacksjs/commerce'
2
4
  import { config } from '@stacksjs/config'
3
5
  import { db } from '@stacksjs/database/runtime'
4
6
  import { response } from '@stacksjs/router'
@@ -62,17 +64,10 @@ export default new Action({
62
64
  .selectAll()
63
65
  .executeTakeFirst()
64
66
 
65
- if (!customer) {
66
- await (db as any)
67
- .insertInto('customers')
68
- .values({ name: cart.shipping_name, email: cart.email })
69
- .execute()
70
- customer = await (db as any)
71
- .selectFrom('customers')
72
- .where('email', '=', cart.email)
73
- .selectAll()
74
- .executeTakeFirst()
75
- }
67
+ // Through commerce's store, which fills what the table requires and the
68
+ // checkout does not ask for, such as the avatar (#3017).
69
+ if (!customer)
70
+ customer = await customers.store({ name: cart.shipping_name, email: cart.email, uuid: randomUUIDv7() } as never)
76
71
 
77
72
  // Integer minor units of the cart's currency, like every amount here.
78
73
  const currency = storefrontCurrency(cart.currency)
@@ -14,9 +14,9 @@ const CART_COOKIE = 'stacks_cart'
14
14
  * `email` on the order against the `email` they're posting with,
15
15
  * and additionally accepts a still-valid cart cookie path for
16
16
  * guest shoppers who haven't enrolled in account auth.
17
- * - Marks `is_verified_purchase=1` only when that email-on-order
17
+ * - Marks `is_verified_purchase` true only when that email-on-order
18
18
  * check passes.
19
- * - Always inserts with `is_approved=0`. The dashboard's reviews
19
+ * - Always inserts with `is_approved` false. The dashboard's reviews
20
20
  * index shows pending entries to admins for moderation.
21
21
  *
22
22
  * Spam handling: rate-limit per IP (5/min) so a hostile actor can't
@@ -123,9 +123,10 @@ export default new Action({
123
123
  rating,
124
124
  title,
125
125
  content,
126
- is_verified_purchase: 1,
127
- is_approved: 0,
128
- is_featured: 0,
126
+ // Booleans, which PostgreSQL requires of these columns (#3005).
127
+ is_verified_purchase: true,
128
+ is_approved: false,
129
+ is_featured: false,
129
130
  helpful_votes: 0,
130
131
  unhelpful_votes: 0,
131
132
  purchase_date: order.created_at || new Date().toISOString(),
@@ -78,6 +78,7 @@ export default defineModel({
78
78
  properties: {
79
79
  required: false,
80
80
  fillable: true,
81
+ type: 'text',
81
82
  validation: {
82
83
  rule: schema.string(),
83
84
  },
@@ -67,6 +67,7 @@ export default defineModel({
67
67
 
68
68
  properties: {
69
69
  fillable: true,
70
+ type: 'text',
70
71
  validation: {
71
72
  rule: schema.string(),
72
73
  },
@@ -21,6 +21,6 @@ export default defineModel({
21
21
  idempotencyKey: { required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.string.uuid() },
22
22
  startedAt: { required: false, fillable: true, validation: { rule: schema.timestamp() }, factory: () => null },
23
23
  finishedAt: { required: false, fillable: true, validation: { rule: schema.timestamp() }, factory: () => null },
24
- error: { required: false, fillable: true, validation: { rule: schema.string() }, factory: () => null },
24
+ error: { type: 'text', required: false, fillable: true, validation: { rule: schema.string() }, factory: () => null },
25
25
  },
26
26
  } as const)
@@ -50,7 +50,6 @@ export default defineModel({
50
50
  order: 1,
51
51
  fillable: true,
52
52
  validation: { rule: schema.number().required() },
53
- factory: () => 1,
54
53
  },
55
54
 
56
55
  name: {
@@ -103,6 +103,7 @@ export default defineModel({
103
103
  text: {
104
104
  required: false,
105
105
  fillable: true,
106
+ type: 'text',
106
107
  validation: {
107
108
  rule: schema.string(),
108
109
  },
@@ -197,7 +198,6 @@ export default defineModel({
197
198
  validation: {
198
199
  rule: schema.number(),
199
200
  },
200
- factory: faker => faker.number.int({ min: 1, max: 8 }),
201
201
  },
202
202
 
203
203
  scheduledAt: {
@@ -17,6 +17,8 @@ export default defineModel({
17
17
  ],
18
18
 
19
19
  traits: {
20
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
21
+ useSeeder: { count: 20 },
20
22
  gdpr: { subject: { via: 'Subscriber' }, erasure: 'anonymize', basis: 'legitimate_interests', purpose: 'Campaign delivery statistics' },
21
23
  useUuid: true,
22
24
  useTimestamps: true,
@@ -37,7 +39,6 @@ export default defineModel({
37
39
  validation: {
38
40
  rule: schema.number(),
39
41
  },
40
- factory: faker => faker.number.int({ min: 1, max: 10 }),
41
42
  },
42
43
 
43
44
  subscriberId: {
@@ -46,7 +47,6 @@ export default defineModel({
46
47
  validation: {
47
48
  rule: schema.number(),
48
49
  },
49
- factory: () => null,
50
50
  },
51
51
 
52
52
  emailListId: {
@@ -55,7 +55,6 @@ export default defineModel({
55
55
  validation: {
56
56
  rule: schema.number(),
57
57
  },
58
- factory: () => null,
59
58
  },
60
59
 
61
60
  status: {
@@ -112,6 +111,7 @@ export default defineModel({
112
111
  error: {
113
112
  required: false,
114
113
  fillable: true,
114
+ type: 'text',
115
115
  validation: {
116
116
  rule: schema.string(),
117
117
  },
@@ -62,7 +62,6 @@ export default defineModel({
62
62
  // query stays single-table. The kanban API keeps it in sync on
63
63
  // card create/move (Phase 2).
64
64
  validation: { rule: schema.number().required() },
65
- factory: () => 1,
66
65
  },
67
66
 
68
67
  title: {
@@ -47,14 +47,12 @@ export default defineModel({
47
47
  order: 1,
48
48
  fillable: true,
49
49
  validation: { rule: schema.number().required() },
50
- factory: () => 1,
51
50
  },
52
51
 
53
52
  userId: {
54
53
  order: 2,
55
54
  fillable: true,
56
55
  validation: { rule: schema.number() },
57
- factory: () => null,
58
56
  },
59
57
 
60
58
  body: {
@@ -16,6 +16,8 @@ export default defineModel({
16
16
  ],
17
17
 
18
18
  traits: {
19
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
20
+ useSeeder: { count: 5 },
19
21
  gdpr: { erasure: 'anonymize', basis: 'legitimate_interests', purpose: 'Byline on published content' },
20
22
  useAuth: {
21
23
  usePasskey: true,
@@ -47,6 +47,7 @@ export default defineModel({
47
47
  required: false,
48
48
  nullable: true,
49
49
  fillable: true,
50
+ type: 'text',
50
51
  validation: { rule: schema.string() },
51
52
  },
52
53
  isActive: {
@@ -25,6 +25,8 @@ export default defineModel({
25
25
  ownership: siteOwnership(),
26
26
 
27
27
  traits: {
28
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
29
+ useSeeder: { count: 3 },
28
30
  useTimestamps: true,
29
31
  useApi: {
30
32
  middleware: ['auth'],
@@ -17,6 +17,8 @@ export default defineModel({
17
17
  ownership: parentOwnership('Menu', 'menu_id'),
18
18
 
19
19
  traits: {
20
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
21
+ useSeeder: { count: 10 },
20
22
  useTimestamps: true,
21
23
  useApi: {
22
24
  middleware: ['auth'],
@@ -25,6 +25,8 @@ export default defineModel({
25
25
  ],
26
26
 
27
27
  traits: {
28
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
29
+ useSeeder: { count: 10 },
28
30
  useTimestamps: true,
29
31
  },
30
32
 
@@ -12,6 +12,8 @@ export default defineModel({
12
12
  ownership: siteOwnership(),
13
13
 
14
14
  traits: {
15
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
16
+ useSeeder: { count: 10 },
15
17
  useUuid: true,
16
18
  useTimestamps: true,
17
19
  useSearch: {
@@ -26,6 +26,8 @@ export default defineModel({
26
26
  ownership: siteOwnership(),
27
27
 
28
28
  traits: {
29
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
30
+ useSeeder: { count: 5 },
29
31
  useTimestamps: true,
30
32
  useApi: {
31
33
  middleware: ['auth'],
@@ -18,6 +18,8 @@ export default defineModel({
18
18
  belongsTo: ['EmailList', 'Subscriber'],
19
19
 
20
20
  traits: {
21
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
22
+ useSeeder: { count: 20 },
21
23
  gdpr: { subject: { via: 'Subscriber' }, erasure: 'delete', basis: 'consent', purpose: 'Mailing list membership' },
22
24
  useUuid: true,
23
25
  useTimestamps: true,
@@ -39,7 +41,6 @@ export default defineModel({
39
41
  validation: {
40
42
  rule: schema.number(),
41
43
  },
42
- factory: faker => faker.number.int({ min: 1, max: 8 }),
43
44
  },
44
45
 
45
46
  subscriberId: {
@@ -48,7 +49,6 @@ export default defineModel({
48
49
  validation: {
49
50
  rule: schema.number(),
50
51
  },
51
- factory: faker => faker.number.int({ min: 1, max: 100 }),
52
52
  },
53
53
 
54
54
  status: {
@@ -15,6 +15,8 @@ export default defineModel({
15
15
  ],
16
16
 
17
17
  traits: {
18
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
19
+ useSeeder: { count: 10 },
18
20
  useTimestamps: true,
19
21
  },
20
22
 
@@ -32,6 +34,7 @@ export default defineModel({
32
34
 
33
35
  message: {
34
36
  fillable: true,
37
+ type: 'text',
35
38
  validation: {
36
39
  rule: schema.string().required(),
37
40
  message: {
@@ -43,6 +46,7 @@ export default defineModel({
43
46
 
44
47
  stack: {
45
48
  fillable: true,
49
+ type: 'text',
46
50
  validation: {
47
51
  rule: schema.string(),
48
52
  message: {
@@ -86,6 +90,7 @@ export default defineModel({
86
90
 
87
91
  additional_info: {
88
92
  fillable: true,
93
+ type: 'text',
89
94
  validation: {
90
95
  rule: schema.string(),
91
96
  message: {
@@ -28,6 +28,8 @@ export default defineModel({
28
28
  ownership: siteOwnership(),
29
29
 
30
30
  traits: {
31
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
32
+ useSeeder: { count: 3 },
31
33
  useUuid: true,
32
34
  useTimestamps: true,
33
35
  useApi: {
@@ -27,6 +27,8 @@ export default defineModel({
27
27
  ownership: parentOwnership('Form', 'form_id'),
28
28
 
29
29
  traits: {
30
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
31
+ useSeeder: { count: 10 },
30
32
  useTimestamps: true,
31
33
  useApi: {
32
34
  middleware: ['auth'],
@@ -18,6 +18,8 @@ export default defineModel({
18
18
  autoIncrement: true,
19
19
 
20
20
  traits: {
21
+ // So a fresh seed fills its dashboard page (stacksjs/stacks#3000).
22
+ useSeeder: { count: 10 },
21
23
  gdpr: { subject: { email: 'email' }, erasure: 'anonymize', basis: 'consent', purpose: 'Form submissions' },
22
24
  useUuid: true,
23
25
  useTimestamps: true,
@@ -47,7 +47,6 @@ export default defineModel({
47
47
  order: 1,
48
48
  fillable: true,
49
49
  validation: { rule: schema.number().required() },
50
- factory: () => 1,
51
50
  },
52
51
 
53
52
  name: {
package/app/Models/Log.ts CHANGED
@@ -49,7 +49,9 @@ export default defineModel({
49
49
  order: 1,
50
50
  fillable: true,
51
51
  validation: {
52
- rule: schema.number().required(),
52
+ // Milliseconds since the epoch, so a bigint column, as on Websocket.time
53
+ // (stacksjs/stacks#2996).
54
+ rule: schema.unix().required(),
53
55
  message: {
54
56
  date: 'Timestamp must be a valid date',
55
57
  },
@@ -15,13 +15,6 @@ export default defineModel({
15
15
  primaryKey: 'id',
16
16
  autoIncrement: true,
17
17
 
18
- indexes: [
19
- {
20
- name: 'magic_link_tokens_token_unique',
21
- columns: ['token'],
22
- unique: true,
23
- },
24
- ],
25
18
 
26
19
  traits: {
27
20
  gdpr: { erasure: 'delete', basis: 'contract', purpose: 'Passwordless sign-in links' },
@@ -28,6 +28,7 @@ export default defineModel({
28
28
 
29
29
  description: {
30
30
  fillable: true,
31
+ type: 'text',
31
32
  validation: {
32
33
  rule: schema.string(),
33
34
  message: {
@@ -32,6 +32,7 @@ export default defineModel({
32
32
 
33
33
  description: {
34
34
  fillable: true,
35
+ type: 'text',
35
36
  validation: {
36
37
  rule: schema.string(),
37
38
  message: {