@stacksjs/defaults 0.72.6 → 0.72.8
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/app/Actions/Auth/GenerateAuthenticationAction.ts +6 -6
- package/app/Actions/Auth/GenerateRegistrationAction.ts +10 -5
- package/app/Actions/Auth/RegisterAction.ts +5 -2
- package/app/Actions/Auth/VerifyAuthenticationAction.ts +5 -3
- package/app/Actions/Auth/VerifyRegistrationAction.ts +7 -3
- package/app/Actions/Billable/CheckoutAction.ts +14 -1
- package/app/Actions/Cms/AuthorDestroyAction.ts +1 -1
- package/app/Actions/Cms/AuthorShowAction.ts +1 -1
- package/app/Actions/Cms/AuthorUpdateAction.ts +1 -1
- package/app/Actions/Cms/CategorizableDestroyAction.ts +1 -1
- package/app/Actions/Cms/CategorizableShowAction.ts +1 -1
- package/app/Actions/Cms/CategorizableUpdateAction.ts +2 -3
- package/app/Actions/Cms/CommentDestroyAction.ts +1 -1
- package/app/Actions/Cms/CommentShowAction.ts +1 -1
- package/app/Actions/Cms/CommentUpdateAction.ts +1 -1
- package/app/Actions/Cms/PageDestroyAction.ts +1 -1
- package/app/Actions/Cms/PageStoreAction.ts +13 -13
- package/app/Actions/Cms/PageUpdateAction.ts +11 -8
- package/app/Actions/Cms/PostDestroyAction.ts +1 -1
- package/app/Actions/Cms/PostShowAction.ts +1 -1
- package/app/Actions/Cms/PostUpdateAction.ts +1 -1
- package/app/Actions/Cms/TaggableDestroyAction.ts +1 -1
- package/app/Actions/Cms/TaggableShowAction.ts +1 -1
- package/app/Actions/Cms/TaggableUpdateAction.ts +2 -2
- package/app/Actions/Commerce/GiftCardUpdateBalanceAction.ts +1 -1
- package/app/Actions/Commerce/WaitlistProductAnalyticsAction.ts +6 -2
- package/app/Actions/Commerce/WaitlistProductQuantityDistributionAction.ts +6 -2
- package/app/Actions/Commerce/WaitlistProductStatusAction.ts +8 -2
- package/app/Actions/Commerce/WaitlistProductTimeSeriesAction.ts +6 -2
- package/app/Actions/Dashboard/Analytics/campaign-analytics.ts +6 -1
- package/app/Actions/Dashboard/Analytics/request-analytics.ts +3 -3
- package/app/Actions/Dashboard/Buddy/buddy-chat.ts +8 -0
- package/app/Actions/Dashboard/Ci/runner-pressure-monitor.ts +2 -1
- package/app/Actions/Dashboard/Cloud/cloud-overview.ts +3 -1
- package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +12 -4
- package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +5 -3
- package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +3 -3
- package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +4 -4
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +20 -9
- package/app/Actions/Dashboard/Content/file-manager.ts +3 -2
- package/app/Actions/Dashboard/DashboardHomeAction.ts +1 -1
- package/app/Actions/Dashboard/DashboardStatsAction.ts +7 -4
- package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +2 -2
- package/app/Actions/Dashboard/Deployments/deployment-input.ts +3 -3
- package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +12 -0
- package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +3 -3
- package/app/Actions/Dashboard/Jobs/job-records.ts +3 -3
- package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +5 -3
- package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +5 -3
- package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +1 -1
- package/app/Actions/Dashboard/Kanban/kanban-model.ts +3 -3
- package/app/Actions/Dashboard/Kanban/kanban-response.ts +2 -1
- package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +1 -0
- package/app/Actions/Dashboard/Library/GetReleases.ts +8 -3
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +3 -3
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +7 -0
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +5 -1
- package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +12 -9
- package/app/Actions/Dashboard/Operations/control-plane.ts +16 -4
- package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +1 -1
- package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +23 -14
- package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -1
- package/app/Actions/Dashboard/Settings/mail-settings.ts +12 -4
- package/app/Actions/Dashboard/Source/source-inventory.ts +6 -2
- package/app/Actions/Dashboard/dashboard-response.ts +2 -1
- package/app/Actions/HealthAction.ts +11 -6
- package/app/Actions/Locale/SetLocaleAction.ts +3 -1
- package/app/Actions/Monitoring/ErrorShowAction.ts +5 -0
- package/app/Actions/Payment/CreatePaymentIntentAction.ts +3 -1
- package/app/Actions/SendWelcomeEmail.ts +3 -1
- package/app/Actions/Teams/team-response.ts +2 -1
- package/app/Actions/TestErrorAction.ts +1 -1
- package/app/Jobs/PruneModelsJob.ts +1 -1
- package/app/Jobs/PruneQueryLogsJob.ts +3 -1
- package/app/Mail/OrderConfirmation.ts +18 -2
- package/app/Middleware/Abilities.ts +3 -1
- package/app/Middleware/Can.ts +3 -3
- package/app/Middleware/Compress.ts +3 -1
- package/app/Middleware/Csrf.ts +10 -4
- package/app/Middleware/EnsureEmailIsVerified.ts +2 -1
- package/app/Middleware/Permission.ts +2 -1
- package/app/Middleware/Role.ts +2 -1
- package/app/Middleware/Team.ts +2 -1
- package/app/Middleware/Throttle.ts +1 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/functions/dashboard/data.ts +2 -2
|
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import {
|
|
4
4
|
generateAuthenticationOptions,
|
|
5
5
|
getUserPasskeys,
|
|
6
|
+
passkeyDescriptors,
|
|
6
7
|
storeWebAuthnChallenge,
|
|
7
8
|
} from '@stacksjs/auth'
|
|
8
9
|
import { config } from '@stacksjs/config'
|
|
@@ -31,13 +32,12 @@ export default new Action({
|
|
|
31
32
|
const appUrl = config.app?.url || 'localhost'
|
|
32
33
|
const rpID = new URL(appUrl.startsWith('http') ? appUrl : `https://${appUrl}`).hostname
|
|
33
34
|
|
|
34
|
-
const options
|
|
35
|
+
const options = await generateAuthenticationOptions({
|
|
35
36
|
rpID,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
37
|
+
// The JSON-vs-ArrayBuffer boundary lives in `passkeyDescriptors`; the
|
|
38
|
+
// narrowing goes away when ts-auth b59ad36 ships.
|
|
39
|
+
allowCredentials: passkeyDescriptors(userPasskeys) as unknown as Parameters<typeof generateAuthenticationOptions>[0]['allowCredentials'],
|
|
40
|
+
}) as unknown as PublicKeyCredentialRequestOptionsJSON
|
|
41
41
|
|
|
42
42
|
// Persist the challenge server-side so `VerifyAuthenticationAction`
|
|
43
43
|
// can consume it from the DB instead of trusting `body.challenge`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { generateRegistrationOptions, getUserPasskeys, storeWebAuthnChallenge } from '@stacksjs/auth'
|
|
2
|
+
import { generateRegistrationOptions, getUserPasskeys, passkeyDescriptors, storeWebAuthnChallenge } from '@stacksjs/auth'
|
|
3
3
|
import { config } from '@stacksjs/config'
|
|
4
4
|
|
|
5
5
|
export default new Action({
|
|
@@ -30,12 +30,17 @@ export default new Action({
|
|
|
30
30
|
const options = await generateRegistrationOptions({
|
|
31
31
|
rpName,
|
|
32
32
|
rpID,
|
|
33
|
+
// The WebAuthn user handle. It was omitted entirely, and the generator
|
|
34
|
+
// does `new TextEncoder().encode(options.userID)` - so every passkey was
|
|
35
|
+
// registered against the handle "undefined", the same one for every
|
|
36
|
+
// user. `storePasskey` records the email as `webauthn_user_id`, so the
|
|
37
|
+
// email is the handle the two halves have to agree on.
|
|
38
|
+
userID: userEmail,
|
|
33
39
|
userName: userEmail,
|
|
34
40
|
attestationType: 'none',
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
})),
|
|
41
|
+
// See `passkeyDescriptors` for the JSON-vs-ArrayBuffer boundary; the
|
|
42
|
+
// narrowing goes away when ts-auth b59ad36 ships.
|
|
43
|
+
excludeCredentials: passkeyDescriptors(userPasskeys) as unknown as Parameters<typeof generateRegistrationOptions>[0]['excludeCredentials'],
|
|
39
44
|
authenticatorSelection: {
|
|
40
45
|
residentKey: 'preferred',
|
|
41
46
|
userVerification: 'preferred',
|
|
@@ -40,11 +40,14 @@ export default new Action({
|
|
|
40
40
|
// — listener errors are caught by the wildcard handler so a flaky
|
|
41
41
|
// welcome email doesn't fail registration. The `to` alias matches
|
|
42
42
|
// the contract SendWelcomeEmail expects.
|
|
43
|
+
// `to` is what SendWelcomeEmail addresses the mail to, so it has to be
|
|
44
|
+
// a string; the registering address is the honest fallback if the
|
|
45
|
+
// freshly-created user could not be read back.
|
|
43
46
|
dispatch('user:registered', {
|
|
44
47
|
id: user?.id,
|
|
45
|
-
email: user?.email,
|
|
48
|
+
email: user?.email ?? email,
|
|
46
49
|
name: user?.name,
|
|
47
|
-
to: user?.email,
|
|
50
|
+
to: user?.email ?? email,
|
|
48
51
|
})
|
|
49
52
|
|
|
50
53
|
// Same OAuth2-compatible payload LoginAction returns, so a client can
|
|
@@ -48,9 +48,11 @@ export default new Action({
|
|
|
48
48
|
}
|
|
49
49
|
const publicKey = new Uint8Array(Object.values(jsonParse)).buffer
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
51
|
+
// `consumeWebAuthnChallenge` already returns the bytes it stored. This
|
|
52
|
+
// used to run them through `atob()`, which stringifies a Uint8Array to
|
|
53
|
+
// "12,34,56..." before decoding it - so the challenge never matched and
|
|
54
|
+
// passkey authentication could not succeed.
|
|
55
|
+
const challengeBytes = expectedChallenge
|
|
54
56
|
|
|
55
57
|
// Derive origin and rpID from app config instead of hardcoding localhost
|
|
56
58
|
const appUrl = config.app?.url || 'http://localhost:3333'
|
|
@@ -40,12 +40,16 @@ export default new Action({
|
|
|
40
40
|
const expectedRPID = new URL(expectedOrigin).hostname
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Positional, as the function is declared. Passing one options object
|
|
44
|
+
// meant `credential.response` was undefined, the read threw, the catch
|
|
45
|
+
// swallowed it and every registration came back `verified: false` -
|
|
46
|
+
// passkey registration could never succeed through this action.
|
|
47
|
+
const verification = await verifyRegistrationResponse(
|
|
48
|
+
body.attResp,
|
|
45
49
|
expectedChallenge,
|
|
46
50
|
expectedOrigin,
|
|
47
51
|
expectedRPID,
|
|
48
|
-
|
|
52
|
+
)
|
|
49
53
|
|
|
50
54
|
await setCurrentRegistrationOptions(user, verification)
|
|
51
55
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserModel } from '@stacksjs/orm'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { manageCheckout } from '@stacksjs/payments'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -18,7 +19,19 @@ export default new Action({
|
|
|
18
19
|
cancel_url: 'https://localhost:3000/cancel',
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// Checkout needs the billable trait, which the default User model leaves
|
|
23
|
+
// OFF ("not every app bills through the User model"). Without it the
|
|
24
|
+
// methods manageCheckout calls - hasStripeId() and friends - are not on
|
|
25
|
+
// the record at all, so say which switch is missing instead of failing
|
|
26
|
+
// deeper in with "user.hasStripeId is not a function".
|
|
27
|
+
if (typeof (user as unknown as Partial<UserModel>).hasStripeId !== 'function') {
|
|
28
|
+
return response.json(
|
|
29
|
+
{ message: 'Billing is not enabled. Set `traits.billable` on the User model to use checkout.' },
|
|
30
|
+
503,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const session = await manageCheckout.create(user as unknown as UserModel, params)
|
|
22
35
|
|
|
23
36
|
return response.json({ url: session.url })
|
|
24
37
|
},
|
|
@@ -7,7 +7,7 @@ export default new Action({
|
|
|
7
7
|
description: 'Delete an author',
|
|
8
8
|
method: 'DELETE',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
const id = request.getParam('id')
|
|
10
|
+
const id = Number(request.getParam('id'))
|
|
11
11
|
const author = await Author.find(Number(id))
|
|
12
12
|
|
|
13
13
|
if (!author) {
|
|
@@ -7,7 +7,7 @@ export default new Action({
|
|
|
7
7
|
description: 'Fetch a single author by ID',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
const id = request.getParam('id')
|
|
10
|
+
const id = Number(request.getParam('id'))
|
|
11
11
|
const result = await Author.find(Number(id))
|
|
12
12
|
|
|
13
13
|
if (!result) {
|
|
@@ -7,7 +7,7 @@ export default new Action({
|
|
|
7
7
|
description: 'Update an existing author',
|
|
8
8
|
method: 'PATCH',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
const id = request.getParam('id')
|
|
10
|
+
const id = Number(request.getParam('id'))
|
|
11
11
|
const author = await Author.find(Number(id))
|
|
12
12
|
|
|
13
13
|
if (!author) {
|
|
@@ -7,7 +7,7 @@ export default new Action({
|
|
|
7
7
|
description: 'Category Show ORM Action',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
const id = request.getParam('id')
|
|
10
|
+
const id = Number(request.getParam('id'))
|
|
11
11
|
|
|
12
12
|
const model = await categorizable.fetchById(id)
|
|
13
13
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CategorizableRequestType } from '@stacksjs/types'
|
|
2
1
|
import { Action } from '@stacksjs/actions'
|
|
3
2
|
import { categorizable } from '@stacksjs/cms'
|
|
4
3
|
import { response } from '@stacksjs/router'
|
|
@@ -24,14 +23,14 @@ export default new Action({
|
|
|
24
23
|
},
|
|
25
24
|
})
|
|
26
25
|
|
|
27
|
-
const id = request.getParam('id')
|
|
26
|
+
const id = Number(request.getParam('id'))
|
|
28
27
|
|
|
29
28
|
const data = {
|
|
30
29
|
name: request.get('name'),
|
|
31
30
|
description: request.get('description'),
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const model = await categorizable.update(id, data)
|
|
33
|
+
const model = await categorizable.update({ id, ...data })
|
|
35
34
|
|
|
36
35
|
return response.json(model)
|
|
37
36
|
},
|
|
@@ -7,7 +7,7 @@ export default new Action({
|
|
|
7
7
|
description: 'Comment Show ORM Action',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
const id = request.getParam('id')
|
|
10
|
+
const id = Number(request.getParam('id'))
|
|
11
11
|
|
|
12
12
|
const model = await comments.fetchCommentById(id)
|
|
13
13
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { pages } from '@stacksjs/cms'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
|
+
import { requireSite } from '@stacksjs/sites'
|
|
4
5
|
|
|
5
6
|
export default new Action({
|
|
6
7
|
name: 'Page Store',
|
|
@@ -10,21 +11,20 @@ export default new Action({
|
|
|
10
11
|
async handle(request) {
|
|
11
12
|
await request.validate()
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// A page belongs to a site, and the site comes from the resolved request
|
|
15
|
+
// context rather than from the body: letting a caller name the site would
|
|
16
|
+
// let them write into somebody else's website.
|
|
17
|
+
const site = requireSite()
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const model = await pages.store({
|
|
20
|
+
site_id: site.id,
|
|
20
21
|
title: request.get('title'),
|
|
22
|
+
slug: request.get('slug'),
|
|
21
23
|
template: request.get('template'),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const model = await pages.store(data)
|
|
24
|
+
meta_description: request.get('meta_description'),
|
|
25
|
+
status: request.get('status') ?? 'draft',
|
|
26
|
+
blocks: request.get('blocks'),
|
|
27
|
+
} as Parameters<typeof pages.store>[0])
|
|
28
28
|
|
|
29
29
|
return response.json(model)
|
|
30
30
|
},
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { pages } from '@stacksjs/cms'
|
|
3
|
-
import { formatDate } from '@stacksjs/orm'
|
|
4
3
|
import { response } from '@stacksjs/router'
|
|
5
4
|
|
|
6
5
|
export default new Action({
|
|
@@ -11,16 +10,20 @@ export default new Action({
|
|
|
11
10
|
async handle(request) {
|
|
12
11
|
await request.validate()
|
|
13
12
|
|
|
14
|
-
const id = request.getParam('id')
|
|
13
|
+
const id = Number(request.getParam('id'))
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// Only what the request actually sent. Spreading every field would write
|
|
16
|
+
// `undefined` over a title the caller never mentioned, and a slug or
|
|
17
|
+
// blocks value that is present goes through the document path in
|
|
18
|
+
// `pages.update`, which re-derives the path and snapshots a revision.
|
|
19
|
+
const data: Record<string, unknown> = {}
|
|
20
|
+
for (const field of ['title', 'slug', 'template', 'meta_description', 'status', 'blocks'] as const) {
|
|
21
|
+
const value = request.get(field)
|
|
22
|
+
if (value !== undefined && value !== null)
|
|
23
|
+
data[field] = value
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
const model = await pages.update(id, data)
|
|
26
|
+
const model = await pages.update(id, data as Parameters<typeof pages.update>[1])
|
|
24
27
|
|
|
25
28
|
return response.json(model)
|
|
26
29
|
},
|
|
@@ -23,14 +23,14 @@ export default new Action({
|
|
|
23
23
|
},
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
const id = request.getParam('id')
|
|
26
|
+
const id = Number(request.getParam('id'))
|
|
27
27
|
const data = {
|
|
28
28
|
id,
|
|
29
29
|
name: request.get('name'),
|
|
30
30
|
description: request.get('description'),
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const model = await tags.update(
|
|
33
|
+
const model = await tags.update(data)
|
|
34
34
|
|
|
35
35
|
return response.json(model)
|
|
36
36
|
},
|
|
@@ -7,8 +7,12 @@ export default new Action({
|
|
|
7
7
|
description: 'WaitlistProduct Analytics Action',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
// Params arrive as strings; `getParam` has no type parameter to change that.
|
|
11
|
+
const startDate = new Date(String(request.getParam('startDate')))
|
|
12
|
+
const endDate = new Date(String(request.getParam('endDate')))
|
|
13
|
+
|
|
14
|
+
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
|
|
15
|
+
return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
|
|
12
16
|
|
|
13
17
|
const [
|
|
14
18
|
countBySource,
|
|
@@ -7,8 +7,12 @@ export default new Action({
|
|
|
7
7
|
description: 'WaitlistProduct Quantity Distribution Action',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
// Params arrive as strings; `getParam` has no type parameter to change that.
|
|
11
|
+
const startDate = new Date(String(request.getParam('startDate')))
|
|
12
|
+
const endDate = new Date(String(request.getParam('endDate')))
|
|
13
|
+
|
|
14
|
+
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
|
|
15
|
+
return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
|
|
12
16
|
|
|
13
17
|
const quantityDistribution = await waitlists.products.fetchCountByAllQuantities(startDate, endDate)
|
|
14
18
|
|
|
@@ -7,8 +7,14 @@ export default new Action({
|
|
|
7
7
|
description: 'WaitlistProduct Status Statistics Action',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Params arrive as strings; `getParam` has no type parameter to change
|
|
11
|
+
// that, and asking for `<Date>` only made the file stop typechecking
|
|
12
|
+
// while still handing the query layer a string.
|
|
13
|
+
const startDate = new Date(String(request.getParam('startDate')))
|
|
14
|
+
const endDate = new Date(String(request.getParam('endDate')))
|
|
15
|
+
|
|
16
|
+
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
|
|
17
|
+
return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
|
|
12
18
|
|
|
13
19
|
const [
|
|
14
20
|
totalCount,
|
|
@@ -7,8 +7,12 @@ export default new Action({
|
|
|
7
7
|
description: 'WaitlistProduct Time Series Action',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
// Params arrive as strings; `getParam` has no type parameter to change that.
|
|
11
|
+
const startDate = new Date(String(request.getParam('startDate')))
|
|
12
|
+
const endDate = new Date(String(request.getParam('endDate')))
|
|
13
|
+
|
|
14
|
+
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
|
|
15
|
+
return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
|
|
12
16
|
|
|
13
17
|
const timeSeriesData = await waitlists.products.fetchCountByDateGrouped(startDate, endDate)
|
|
14
18
|
|
|
@@ -115,7 +115,12 @@ export function buildCampaignAnalytics(
|
|
|
115
115
|
}
|
|
116
116
|
const channels = [...channelMap.entries()]
|
|
117
117
|
.map(([key, rows]) => {
|
|
118
|
-
|
|
118
|
+
// `split` gives `string | undefined` per element; the keys are built as
|
|
119
|
+
// `type:currency` a few lines up, so an empty string is the honest
|
|
120
|
+
// fallback for a malformed one rather than a crash in `titleCase`.
|
|
121
|
+
const [rawType, rawCurrency] = key.split(':')
|
|
122
|
+
const type = rawType ?? ''
|
|
123
|
+
const currency = rawCurrency ?? ''
|
|
119
124
|
return {
|
|
120
125
|
type,
|
|
121
126
|
name: titleCase(type),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReadableRecord } from '@stacksjs/orm'
|
|
1
2
|
export type AnalyticsRange = 'day' | 'week' | 'month' | 'year'
|
|
2
3
|
export type AnalyticsScope = 'all' | 'blog' | 'commerce'
|
|
3
4
|
|
|
@@ -11,9 +12,8 @@ export interface RequestAnalyticsRow {
|
|
|
11
12
|
createdAt: string
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
15
|
+
/** The shared shape, kept under this name for the helpers below. */
|
|
16
|
+
export type AnalyticsModelRecord = ReadableRecord
|
|
17
17
|
|
|
18
18
|
interface TrafficBucket {
|
|
19
19
|
date: string
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { AIMessage, AIProvider, ConfiguredAIOptions } from '@stacksjs/ai'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The two keys this check reads out of the environment.
|
|
5
|
+
*
|
|
6
|
+
* The index signature is what makes `process.env` assignable: `ProcessEnv`
|
|
7
|
+
* declares no named members, so a bare two-key interface shares nothing with
|
|
8
|
+
* it and TypeScript rejects the call - the one thing every caller passes.
|
|
9
|
+
*/
|
|
3
10
|
export interface BuddyProviderEnvironment {
|
|
4
11
|
ANTHROPIC_API_KEY?: string
|
|
5
12
|
OPENAI_API_KEY?: string
|
|
13
|
+
[key: string]: string | undefined
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
export function resolveBuddyProvider(config: ConfiguredAIOptions): AIProvider {
|
|
@@ -53,7 +53,8 @@ async function appendSamples(snapshot: DashboardData, nowIso: string): Promise<v
|
|
|
53
53
|
// Insert one row per org. Cross-dialect multi-row INSERT keeps it
|
|
54
54
|
// a single round-trip even for the typical 5-org fleet.
|
|
55
55
|
const rows = orgs.map((org) => {
|
|
56
|
-
|
|
56
|
+
// An org with no sample yet reads as zero pressure rather than throwing.
|
|
57
|
+
const r = runners[org] ?? { running: 0, queued: 0, cap: 0 }
|
|
57
58
|
return { org, running: r.running, queued: r.queued, cap: r.cap, sampled_at: nowIso }
|
|
58
59
|
})
|
|
59
60
|
await db.insertInto('ci_runner_samples').values(rows as any).execute()
|
|
@@ -420,7 +420,9 @@ function serverlessOverview(config: CloudRecord): {
|
|
|
420
420
|
serviceDetails: DashboardCloudResourceDetail[] = [],
|
|
421
421
|
) => {
|
|
422
422
|
const id = `serverless:${environmentName}:${suffix}`
|
|
423
|
-
|
|
423
|
+
// `Record<string, …>` makes every lookup optional; an unknown service
|
|
424
|
+
// type gets a neutral chip rather than crashing the whole overview.
|
|
425
|
+
const presentation = servicePresentation[type] ?? { color: '#64748B', icon: 'i-hugeicons-cube' }
|
|
424
426
|
services.push({
|
|
425
427
|
id,
|
|
426
428
|
name,
|
|
@@ -35,15 +35,23 @@ export default new Action({
|
|
|
35
35
|
const customerIds = [...new Set(orderRows
|
|
36
36
|
.map(order => order.customerId)
|
|
37
37
|
.filter(Boolean))]
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
// The normalizers hand back identifiers as strings (a record may key on
|
|
40
|
+
// a uuid), but these columns are numeric, so they go back to numbers at
|
|
41
|
+
// the query boundary - the same conversion CommerceOrdersAction does.
|
|
42
|
+
const numericOrderIds = orderIds.map(Number).filter(Number.isSafeInteger)
|
|
43
|
+
const numericCustomerIds = customerIds.map(Number).filter(Number.isSafeInteger)
|
|
44
|
+
|
|
45
|
+
const orderItems = numericOrderIds.length > 0
|
|
46
|
+
? await OrderItem.whereIn('order_id', numericOrderIds).get()
|
|
40
47
|
: []
|
|
41
48
|
const orderItemRows = orderItems.map(normalizeCommerceDashboardOrderItem)
|
|
42
49
|
const productIds = [...new Set(orderItemRows.map(item => item.productId))]
|
|
50
|
+
const numericProductIds = productIds.map(Number).filter(Number.isSafeInteger)
|
|
43
51
|
|
|
44
52
|
const [products, customers] = await Promise.all([
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
numericProductIds.length > 0 ? Product.whereIn('id', numericProductIds).get() : [],
|
|
54
|
+
numericCustomerIds.length > 0 ? Customer.whereIn('id', numericCustomerIds).get() : [],
|
|
47
55
|
])
|
|
48
56
|
|
|
49
57
|
return buildCommerceDashboard(
|
|
@@ -28,7 +28,7 @@ export default new Action({
|
|
|
28
28
|
const [customers, coupons, items] = await Promise.all([
|
|
29
29
|
Customer.orderBy('name', 'asc').limit(500).get(),
|
|
30
30
|
Coupon.orderBy('id', 'asc').limit(500).get(),
|
|
31
|
-
numericOrderIds.length > 0 ? OrderItem.
|
|
31
|
+
numericOrderIds.length > 0 ? OrderItem.whereIn('order_id', numericOrderIds).get() : [],
|
|
32
32
|
])
|
|
33
33
|
const customerContexts = customers.map(normalizeOrderCustomerContext)
|
|
34
34
|
const customerMap = new Map(customerContexts.map(customer => [customer.id, customer.context]))
|
|
@@ -103,7 +103,7 @@ async function existingReceipt(order: any) {
|
|
|
103
103
|
receiptId(field(item, 'product_id', 'productId'), `OrderItem ${index + 1}`, 'product_id'),
|
|
104
104
|
)
|
|
105
105
|
const [products, payments] = await Promise.all([
|
|
106
|
-
productIds.length > 0 ? Product.
|
|
106
|
+
productIds.length > 0 ? Product.whereIn('id', productIds).get() : [],
|
|
107
107
|
Payment.where('order_id', orderId).get(),
|
|
108
108
|
])
|
|
109
109
|
const names = new Map(products.map((product) => {
|
|
@@ -242,7 +242,7 @@ export default new Action({
|
|
|
242
242
|
let taxRates
|
|
243
243
|
try {
|
|
244
244
|
const [productRows, categories, manufacturers, persistedTaxRates] = await Promise.all([
|
|
245
|
-
Product.
|
|
245
|
+
Product.whereIn('id', productIds).get(),
|
|
246
246
|
Category.orderBy('name', 'asc').limit(500).get(),
|
|
247
247
|
Manufacturer.orderBy('manufacturer', 'asc').limit(500).get(),
|
|
248
248
|
TaxRate.orderBy('id', 'asc').limit(500).get(),
|
|
@@ -93,7 +93,7 @@ export default new Action({
|
|
|
93
93
|
const printDeviceIds = new Set(printDevices.map(device =>
|
|
94
94
|
commerceIdentifier(commerceValue(device, 'id', 'uuid'), 'PrintDevice'),
|
|
95
95
|
))
|
|
96
|
-
const records = rows.map(row => normalizePrintLogRecord(row, printDeviceIds))
|
|
96
|
+
const records = rows.map((row: Record<string, unknown>) => normalizePrintLogRecord(row, printDeviceIds))
|
|
97
97
|
|
|
98
98
|
return {
|
|
99
99
|
records,
|