@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.
- package/ai/skills/stacks-browse/SKILL.md +8 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +16 -6
- package/ai/skills/stacks-browse/scripts/chrome.ts +4 -0
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-desktop/SKILL.md +23 -0
- package/ai/skills/stacks-migrations/SKILL.md +2 -2
- package/ai/skills/stacks-native/CATALOG.md +8 -8
- package/app/Actions/Cms/AuthorUpdateAction.ts +4 -4
- package/app/Actions/Cms/CategorizableUpdateAction.ts +4 -4
- package/app/Actions/Cms/PostUpdateAction.ts +4 -5
- package/app/Actions/Cms/TaggableUpdateAction.ts +4 -2
- package/app/Actions/Commerce/CustomerStoreAction.ts +4 -6
- package/app/Actions/Commerce/CustomerUpdateAction.ts +4 -6
- package/app/Actions/Commerce/PaymentStoreAction.ts +5 -8
- package/app/Actions/Commerce/PaymentUpdateAction.ts +20 -17
- package/app/Actions/Dashboard/Commerce/commerce-product-records.test.ts +7 -0
- package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +14 -3
- package/app/Actions/Dashboard/Commerce/review-records.test.ts +12 -0
- package/app/Actions/Dashboard/Commerce/review-records.ts +10 -5
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +2 -0
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +4 -1
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +2 -0
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +2 -0
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +2 -0
- package/app/Actions/Dashboard/Content/content-input.ts +9 -4
- package/app/Actions/Dashboard/Content/content-record-input.test.ts +20 -1
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Models/ModelStoreAction.ts +5 -1
- package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +5 -1
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction.ts +1 -1
- package/app/Actions/Storefront/AddToCartAction.ts +5 -2
- package/app/Actions/Storefront/PlaceOrderAction.ts +6 -11
- package/app/Actions/Storefront/SubmitReviewAction.ts +6 -5
- package/app/Models/Activity.ts +1 -0
- package/app/Models/AnalyticsEvent.ts +1 -0
- package/app/Models/AutomationRun.ts +1 -1
- package/app/Models/BoardColumn.ts +0 -1
- package/app/Models/Campaign.ts +1 -1
- package/app/Models/CampaignSend.ts +3 -3
- package/app/Models/Card.ts +0 -1
- package/app/Models/CardComment.ts +0 -2
- package/app/Models/Content/Author.ts +2 -0
- package/app/Models/Content/Categorizable.ts +1 -0
- package/app/Models/Content/Menu.ts +2 -0
- package/app/Models/Content/MenuItem.ts +2 -0
- package/app/Models/Content/PageRevision.ts +2 -0
- package/app/Models/Content/Post.ts +2 -0
- package/app/Models/Content/Redirect.ts +2 -0
- package/app/Models/EmailListSubscriber.ts +2 -2
- package/app/Models/Error.ts +5 -0
- package/app/Models/Forms/Form.ts +2 -0
- package/app/Models/Forms/FormField.ts +2 -0
- package/app/Models/Forms/FormSubmission.ts +2 -0
- package/app/Models/Label.ts +0 -1
- package/app/Models/Log.ts +3 -1
- package/app/Models/MagicLinkToken.ts +0 -7
- package/app/Models/PaymentProduct.ts +1 -0
- package/app/Models/PaymentTransaction.ts +1 -0
- package/app/Models/Release.ts +3 -0
- package/app/Models/Request.ts +1 -0
- package/app/Models/SenderDomain.ts +2 -0
- package/app/Models/SmsOptOut.ts +0 -7
- package/app/Models/Subscription.ts +2 -0
- package/app/Models/commerce/Auction.ts +1 -0
- package/app/Models/commerce/AuctionItem.ts +1 -0
- package/app/Models/commerce/Cart.ts +83 -2
- package/app/Models/commerce/Category.ts +5 -1
- package/app/Models/commerce/Coupon.ts +4 -1
- package/app/Models/commerce/DigitalDelivery.ts +1 -0
- package/app/Models/commerce/GiftCard.ts +1 -0
- package/app/Models/commerce/LoyaltyPoint.ts +1 -0
- package/app/Models/commerce/LoyaltyReward.ts +3 -1
- package/app/Models/commerce/Order.ts +4 -1
- package/app/Models/commerce/OrderItem.ts +1 -0
- package/app/Models/commerce/Payment.ts +1 -0
- package/app/Models/commerce/Product.ts +15 -0
- package/app/Models/commerce/ProductVariant.ts +2 -0
- package/app/Models/commerce/Receipt.ts +1 -0
- package/app/Models/commerce/Review.ts +3 -1
- package/app/Models/commerce/Transaction.ts +1 -0
- package/app/Models/realtime/Websocket.ts +4 -1
- package/functions/dashboard-form.test.ts +10 -1
- package/functions/dashboard-form.ts +17 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/Analytics/AnalyticsPageHeader.stx +1 -1
- package/resources/components/Dashboard/App/RequestDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +1 -1
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +1 -1
- package/resources/components/Dashboard/Auth/Login.stx +1 -1
- package/resources/components/Dashboard/Auth/Register.stx +1 -1
- package/resources/components/Dashboard/Auth/ResetPassword.stx +1 -1
- package/resources/components/Dashboard/Auth/TwoFactorLogin.stx +1 -1
- package/resources/components/Dashboard/CodeEditor.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +4 -13
- package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceManufacturersTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePosCart.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +27 -2
- package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +7 -5
- package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +12 -4
- package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +3 -3
- package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/CourierDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/CourierDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/CouriersTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryPagination.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintDevicesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintLogsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/ProductWaitlistTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistTable.stx +1 -1
- package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +1 -1
- package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +1 -1
- package/resources/components/Dashboard/Content/CreateFolderDialog.stx +1 -1
- package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +1 -1
- package/resources/components/Dashboard/Content/FileUploadDialog.stx +1 -1
- package/resources/components/Dashboard/Content/PageEditorDialog.stx +1 -1
- package/resources/components/Dashboard/Content/PostEditorDialog.stx +1 -1
- package/resources/components/Dashboard/Content/SeoEditorDialog.stx +1 -1
- package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +1 -1
- package/resources/components/Dashboard/CreateRecordModal.stx +1 -1
- package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +1 -1
- package/resources/components/Dashboard/Elements/DropdownNavigation.stx +1 -1
- package/resources/components/Dashboard/Email/CapturedMailList.stx +1 -1
- package/resources/components/Dashboard/Email/EmailSettingsList.stx +1 -1
- package/resources/components/Dashboard/Email/EmailSettingsNavigation.stx +1 -1
- package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
- package/resources/components/Dashboard/Email/InboxComposer.stx +1 -1
- package/resources/components/Dashboard/Email/InboxMessageDetail.stx +1 -1
- package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/CloudResourceButton.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +1 -1
- package/resources/components/Dashboard/Jobs/JobTable.stx +1 -1
- package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +1 -1
- package/resources/components/Dashboard/Management/AssignmentDrawer.stx +1 -1
- package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/CampaignDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/CampaignsTable.stx +1 -1
- package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
- package/resources/components/Dashboard/Marketing/RecoveryCampaignDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +1 -1
- package/resources/components/Dashboard/Marketing/SocialPostsTable.stx +1 -1
- package/resources/components/Dashboard/MobileSidebar.stx +1 -1
- package/resources/components/Dashboard/Modals/BaseModal.stx +1 -1
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +1 -1
- package/resources/components/Dashboard/Modals/Popups/Toast.stx +1 -1
- package/resources/components/Dashboard/Modals/ToastWrapper.stx +1 -1
- package/resources/components/Dashboard/Models/EditRecordModal.stx +1 -1
- package/resources/components/Dashboard/Models/ModelRecordActions.stx +1 -1
- package/resources/components/Dashboard/Monitoring/ErrorTable.stx +1 -1
- package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +1 -1
- package/resources/components/Dashboard/Notifications/NotificationDeliveryTable.stx +1 -1
- package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +1 -1
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +1 -1
- package/resources/components/Dashboard/Operations/RecoveryPolicyDialog.stx +1 -1
- package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +1 -1
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +1 -1
- package/resources/components/Dashboard/UI/Checkbox.stx +1 -1
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +1 -1
- package/resources/components/Dashboard/UI/Drawer.stx +1 -1
- package/resources/components/Dashboard/UI/Dropdown.stx +1 -1
- package/resources/components/Dashboard/UI/Input.stx +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +1 -1
- package/resources/components/Dashboard/UI/Pagination.stx +1 -1
- package/resources/components/Dashboard/UI/SearchFilter.stx +1 -1
- package/resources/components/Dashboard/UI/Select.stx +1 -1
- package/resources/components/Dashboard/UI/Table.stx +1 -1
- package/resources/components/Dashboard/UI/Tabs.stx +1 -1
- package/resources/components/Dashboard/UI/Textarea.stx +1 -1
- package/resources/components/Dashboard/UI/Toggle.stx +1 -1
- package/resources/components/Dashboard/UI/WindowControls.stx +1 -1
- package/resources/components/NativeActionSheet.stx +1 -1
- package/resources/components/NativeContextMenu.stx +1 -1
- package/resources/components/NativeHealthButton.stx +1 -1
- package/resources/components/NativePermissionButton.stx +1 -1
- package/resources/components/NativePullToRefresh.stx +1 -1
- package/resources/components/NativeSegmentedControl.stx +1 -1
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
146
|
-
? Math.round(totalRating /
|
|
150
|
+
averageRating: ratings.length > 0
|
|
151
|
+
? Math.round(totalRating / ratings.length * 10) / 10
|
|
147
152
|
: 0,
|
|
148
153
|
}
|
|
149
154
|
}
|
|
@@ -53,11 +53,14 @@ export default new Action({
|
|
|
53
53
|
name,
|
|
54
54
|
slug,
|
|
55
55
|
description,
|
|
56
|
-
|
|
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)
|
|
@@ -20,11 +20,16 @@ export function timestamp(): string {
|
|
|
20
20
|
return new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
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
|
|
26
|
-
|
|
27
|
-
return Number(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
17
|
+
* - Marks `is_verified_purchase` true only when that email-on-order
|
|
18
18
|
* check passes.
|
|
19
|
-
* - Always inserts with `is_approved
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(),
|
package/app/Models/Activity.ts
CHANGED
|
@@ -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)
|
package/app/Models/Campaign.ts
CHANGED
|
@@ -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
|
},
|
package/app/Models/Card.ts
CHANGED
|
@@ -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,
|
|
@@ -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: {
|
package/app/Models/Error.ts
CHANGED
|
@@ -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: {
|
package/app/Models/Forms/Form.ts
CHANGED
|
@@ -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,
|
package/app/Models/Label.ts
CHANGED
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
|
-
|
|
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' },
|