@stacksjs/defaults 0.70.356 → 0.70.358
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 +5 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
- package/ai/skills/stacks-orm/SKILL.md +6 -0
- package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
- package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
- package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
- package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
- package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
- package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
- package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
- package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
- package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
- package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
- package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
- package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
- package/app/Actions/Dashboard/Content/content-input.ts +4 -4
- package/app/Actions/Dashboard/Content/post-input.ts +60 -8
- package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
- package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
- package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
- package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
- package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
- package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
- package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
- package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
- package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
- package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
- package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
- package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
- package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
- package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
- package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
- package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
- package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
- package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
- package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
- package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
- package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
- package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +60 -10
- package/views/dashboard/stores/auth.ts +18 -2
|
@@ -100,13 +100,17 @@ Extracts headings, links (`text -> href`), buttons, forms (action + field count)
|
|
|
100
100
|
```bash
|
|
101
101
|
bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts scenario <url> \
|
|
102
102
|
--step '{"action":"click","selector":"button[data-open]"}' \
|
|
103
|
+
--step '{"action":"focus","selector":"input[name=name]"}' \
|
|
103
104
|
--step '{"action":"fill","selector":"input[name=name]","value":"Example"}' \
|
|
104
105
|
--step '{"action":"click","selector":"button[type=submit]"}' \
|
|
105
106
|
--step '{"action":"assert","selector":"main","text":"Saved"}' \
|
|
107
|
+
--step '{"action":"assert","selector":"[role=dialog]","absent":true}' \
|
|
106
108
|
--out storage/framework/runtime/shots/scenario.png
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
Steps run in order within one isolated browser page, preserving reactive STX state and SPA navigation. Supported actions are `click`, `fill`, `press`, `wait`, and `assert`. Each step is a JSON object passed through a repeatable `--step` flag.
|
|
111
|
+
Steps run in order within one isolated browser page, preserving reactive STX state and SPA navigation. Supported actions are `click`, `fill`, `focus`, `press`, `wait`, and `assert`. Each step is a JSON object passed through a repeatable `--step` flag. Click and focus steps may include `text` to select the matching control from their CSS selector. The command reports every completed step, the final URL and page text, console messages, console exceptions, failed requests, and an optional screenshot. It exits nonzero when the scenario assertion, browser console, or network fails.
|
|
112
|
+
Use `{"action":"assert","selector":"...","absent":true}` to verify that an element is missing or hidden after an interaction.
|
|
113
|
+
Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to verify keyboard focus and focus restoration.
|
|
110
114
|
|
|
111
115
|
### Crawl (whole-site browser audit)
|
|
112
116
|
```bash
|
|
@@ -497,12 +497,14 @@ function parseViewport(value: string | undefined): { w: number, h: number } {
|
|
|
497
497
|
return { w, h }
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
type ScenarioAction = 'assert' | 'click' | 'fill' | 'press' | 'wait'
|
|
500
|
+
type ScenarioAction = 'assert' | 'click' | 'fill' | 'focus' | 'press' | 'wait'
|
|
501
501
|
|
|
502
502
|
interface ScenarioStep {
|
|
503
503
|
action: ScenarioAction
|
|
504
504
|
selector?: string
|
|
505
505
|
text?: string
|
|
506
|
+
absent?: boolean
|
|
507
|
+
focused?: boolean
|
|
506
508
|
value?: string
|
|
507
509
|
key?: string
|
|
508
510
|
ms?: number
|
|
@@ -522,14 +524,18 @@ function parseScenarioStep(value: string): ScenarioStep {
|
|
|
522
524
|
throw new TypeError('Each scenario step must be a JSON object.')
|
|
523
525
|
|
|
524
526
|
const step = parsed as Record<string, unknown>
|
|
525
|
-
if (!['assert', 'click', 'fill', 'press', 'wait'].includes(String(step.action)))
|
|
527
|
+
if (!['assert', 'click', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
|
|
526
528
|
throw new TypeError(`Unsupported scenario action: ${String(step.action)}`)
|
|
527
529
|
|
|
528
530
|
const action = step.action as ScenarioAction
|
|
529
|
-
if (['assert', 'click', 'fill'].includes(action) && typeof step.selector !== 'string')
|
|
531
|
+
if (['assert', 'click', 'fill', 'focus'].includes(action) && typeof step.selector !== 'string')
|
|
530
532
|
throw new TypeError(`Scenario action "${action}" requires a selector.`)
|
|
531
533
|
if (action === 'fill' && typeof step.value !== 'string')
|
|
532
534
|
throw new TypeError('Scenario action "fill" requires a string value.')
|
|
535
|
+
if (step.absent !== undefined && (action !== 'assert' || typeof step.absent !== 'boolean'))
|
|
536
|
+
throw new TypeError('Scenario "absent" is a boolean supported only by assert actions.')
|
|
537
|
+
if (step.focused !== undefined && (action !== 'assert' || typeof step.focused !== 'boolean'))
|
|
538
|
+
throw new TypeError('Scenario "focused" is a boolean supported only by assert actions.')
|
|
533
539
|
if (action === 'press' && typeof step.key !== 'string')
|
|
534
540
|
throw new TypeError('Scenario action "press" requires a key.')
|
|
535
541
|
if (action === 'wait' && (!Number.isFinite(step.ms) || Number(step.ms) < 0 || Number(step.ms) > 30_000))
|
|
@@ -557,23 +563,36 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
|
|
|
557
563
|
const step = ${JSON.stringify(step)}
|
|
558
564
|
const candidates = Array.from(document.querySelectorAll(step.selector))
|
|
559
565
|
const normalizedText = (element) => (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
560
|
-
const element = step.action === 'click' && step.text !== undefined
|
|
566
|
+
const element = (step.action === 'click' || step.action === 'focus' || step.action === 'assert') && step.text !== undefined
|
|
561
567
|
? candidates.find(candidate => normalizedText(candidate) === step.text)
|
|
562
568
|
|| candidates.find(candidate => normalizedText(candidate).includes(step.text))
|
|
563
569
|
: candidates[0]
|
|
570
|
+
if (!element && step.action === 'assert' && step.absent)
|
|
571
|
+
return { ok: true, action: step.action, selector: step.selector, absent: true }
|
|
564
572
|
if (!element)
|
|
565
573
|
return { ok: false, error: 'Element not found', selector: step.selector }
|
|
566
574
|
|
|
567
575
|
const rect = element.getBoundingClientRect()
|
|
568
576
|
const style = getComputedStyle(element)
|
|
569
577
|
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'
|
|
578
|
+
if (step.action === 'assert' && step.absent) {
|
|
579
|
+
if (visible)
|
|
580
|
+
return { ok: false, error: 'Expected element to be absent', selector: step.selector }
|
|
581
|
+
return { ok: true, action: step.action, selector: step.selector, absent: true }
|
|
582
|
+
}
|
|
570
583
|
if (!visible)
|
|
571
584
|
return { ok: false, error: 'Element is not visible', selector: step.selector }
|
|
572
585
|
|
|
573
586
|
if (step.action === 'click') {
|
|
574
587
|
if (element.disabled || element.getAttribute('aria-disabled') === 'true')
|
|
575
588
|
return { ok: false, error: 'Element is disabled', selector: step.selector }
|
|
576
|
-
|
|
589
|
+
}
|
|
590
|
+
else if (step.action === 'focus') {
|
|
591
|
+
if (typeof element.focus !== 'function')
|
|
592
|
+
return { ok: false, error: 'Element cannot receive focus', selector: step.selector }
|
|
593
|
+
element.focus()
|
|
594
|
+
if (document.activeElement !== element)
|
|
595
|
+
return { ok: false, error: 'Element did not receive focus', selector: step.selector }
|
|
577
596
|
}
|
|
578
597
|
else if (step.action === 'fill') {
|
|
579
598
|
if (!('value' in element))
|
|
@@ -593,12 +612,19 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
|
|
|
593
612
|
return { ok: false, error: 'Expected text was not found', selector: step.selector, expected: step.text, actual: content.slice(0, 240) }
|
|
594
613
|
}
|
|
595
614
|
|
|
615
|
+
if (step.action === 'assert' && step.focused !== undefined) {
|
|
616
|
+
const focused = document.activeElement === element
|
|
617
|
+
if (focused !== step.focused)
|
|
618
|
+
return { ok: false, error: step.focused ? 'Expected element to be focused' : 'Expected element not to be focused', selector: step.selector }
|
|
619
|
+
}
|
|
620
|
+
|
|
596
621
|
return {
|
|
597
622
|
ok: true,
|
|
598
623
|
action: step.action,
|
|
599
624
|
selector: step.selector,
|
|
600
625
|
tag: element.tagName.toLowerCase(),
|
|
601
626
|
text: normalizedText(element).slice(0, 160),
|
|
627
|
+
clickPoint: step.action === 'click' ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : undefined,
|
|
602
628
|
}
|
|
603
629
|
})()`,
|
|
604
630
|
returnByValue: true,
|
|
@@ -606,6 +632,12 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
|
|
|
606
632
|
const value = result.result?.value as Record<string, unknown> | undefined
|
|
607
633
|
if (!value?.ok)
|
|
608
634
|
throw new Error(`Scenario ${step.action} failed: ${String(value?.error || 'unknown error')} (${step.selector || step.key || ''})`)
|
|
635
|
+
if (step.action === 'click') {
|
|
636
|
+
const point = value.clickPoint as { x: number, y: number }
|
|
637
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 })
|
|
638
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 1 })
|
|
639
|
+
delete value.clickPoint
|
|
640
|
+
}
|
|
609
641
|
return value
|
|
610
642
|
}
|
|
611
643
|
|
|
@@ -741,6 +773,20 @@ async function main() {
|
|
|
741
773
|
const viewport = parseViewport(typeof flags.viewport === 'string' ? flags.viewport : undefined)
|
|
742
774
|
const state = await gotoAndInstrument(cdp, url, { viewport, cookies, settleMs, scheme })
|
|
743
775
|
const results: Record<string, unknown>[] = []
|
|
776
|
+
await cdp.send('Page.bringToFront')
|
|
777
|
+
await cdp.send('Runtime.evaluate', {
|
|
778
|
+
expression: `(() => {
|
|
779
|
+
window.__browseFocusHistory = []
|
|
780
|
+
const describe = (element) => element ? {
|
|
781
|
+
tag: element.tagName?.toLowerCase() || null,
|
|
782
|
+
role: element.getAttribute?.('role') || null,
|
|
783
|
+
ariaLabel: element.getAttribute?.('aria-label') || null,
|
|
784
|
+
text: (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 80),
|
|
785
|
+
} : null
|
|
786
|
+
document.addEventListener('focusin', event => window.__browseFocusHistory.push({ event: 'focusin', target: describe(event.target) }), true)
|
|
787
|
+
document.addEventListener('focusout', event => window.__browseFocusHistory.push({ event: 'focusout', target: describe(event.target), related: describe(event.relatedTarget) }), true)
|
|
788
|
+
})()`,
|
|
789
|
+
})
|
|
744
790
|
|
|
745
791
|
for (const step of steps) {
|
|
746
792
|
results.push(await runScenarioStep(cdp, step))
|
|
@@ -752,7 +798,15 @@ async function main() {
|
|
|
752
798
|
expression: `({
|
|
753
799
|
url: location.href,
|
|
754
800
|
title: document.title,
|
|
755
|
-
|
|
801
|
+
focusHistory: window.__browseFocusHistory || [],
|
|
802
|
+
activeElement: document.activeElement ? {
|
|
803
|
+
tag: document.activeElement.tagName?.toLowerCase() || null,
|
|
804
|
+
id: document.activeElement.id || null,
|
|
805
|
+
role: document.activeElement.getAttribute?.('role') || null,
|
|
806
|
+
ariaLabel: document.activeElement.getAttribute?.('aria-label') || null,
|
|
807
|
+
ref: document.activeElement.getAttribute?.('data-stx-ref') || null,
|
|
808
|
+
text: (document.activeElement.innerText || document.activeElement.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 120),
|
|
809
|
+
} : null,
|
|
756
810
|
bodyText: (document.body.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 500),
|
|
757
811
|
})`,
|
|
758
812
|
returnByValue: true,
|
|
@@ -770,6 +824,7 @@ async function main() {
|
|
|
770
824
|
steps: results,
|
|
771
825
|
final: finalState.result?.value,
|
|
772
826
|
screenshot,
|
|
827
|
+
consoleMessages: state.console,
|
|
773
828
|
consoleErrors: state.consoleErrors,
|
|
774
829
|
failedRequests,
|
|
775
830
|
}, null, 2))
|
|
@@ -196,6 +196,12 @@ await createUser('Alice', 'alice@example.com') // auto-wrapped
|
|
|
196
196
|
|
|
197
197
|
Both `transaction()` and `savepoint()` delegate to `db.transaction()` and `db.savepoint()` from `@stacksjs/database`.
|
|
198
198
|
|
|
199
|
+
### Transaction executor boundary
|
|
200
|
+
|
|
201
|
+
Every query that must commit or roll back together must use the callback handle (`tx` or `sp`), including validation reads, pivot writes, and the final readback. Do not mix `Model.find()`, `Model.create()`, instance `update()` / `delete()`, or instance relation calls into a raw query-builder transaction. The model executor is a separate execution surface and is not rebound to the callback handle. On SQLite it may use a separate connection, so it cannot observe an uncommitted row written through `tx`.
|
|
202
|
+
|
|
203
|
+
`runInTransactionScope()` buffers supported side effects until commit, but it does not rebind model queries. For a transaction-backed custom action, use the model definition as the schema and relationship source of truth, then execute the complete persistence workflow through `tx`. Read the created or updated row through `tx` before returning so a readback failure also rolls back the mutation.
|
|
204
|
+
|
|
199
205
|
## Trait Methods (traits/)
|
|
200
206
|
|
|
201
207
|
### Taggable (when `traits.taggable: true`)
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import process from 'node:process'
|
|
3
3
|
import { discoverActionSources } from '../Source/source-inventory'
|
|
4
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
5
|
|
|
5
6
|
export default new Action({
|
|
6
7
|
name: 'GetActions',
|
|
7
8
|
description: 'Lists application and framework Actions from their native source files.',
|
|
8
9
|
method: 'GET',
|
|
9
10
|
async handle() {
|
|
10
|
-
|
|
11
|
+
try {
|
|
12
|
+
const items = await discoverActionSources(process.cwd())
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
return {
|
|
15
|
+
items,
|
|
16
|
+
stats: {
|
|
17
|
+
total: items.length,
|
|
18
|
+
application: items.filter(item => item.origin === 'Application').length,
|
|
19
|
+
framework: items.filter(item => item.origin === 'Framework').length,
|
|
20
|
+
writes: items.filter(item => !['ANY', 'GET', 'HEAD', 'OPTIONS'].includes(item.method || 'ANY')).length,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return dashboardOperationalError(error, 'Action sources could not be loaded.', 'GetActions')
|
|
20
26
|
}
|
|
21
27
|
},
|
|
22
28
|
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
|
-
import { AnalyticsEvent } from '@stacksjs/orm'
|
|
3
|
+
import { AnalyticsEvent, ModelValidationError } from '@stacksjs/orm'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
6
|
|
|
6
7
|
function token(value: unknown, fallback: string): string {
|
|
7
8
|
const normalized = String(value || '')
|
|
@@ -46,14 +47,25 @@ export default new Action({
|
|
|
46
47
|
if (properties.length > 10_000)
|
|
47
48
|
return response.json({ message: 'Properties must be 10,000 characters or fewer.' }, 422)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
try {
|
|
51
|
+
await AnalyticsEvent.create({
|
|
52
|
+
name,
|
|
53
|
+
category,
|
|
54
|
+
path,
|
|
55
|
+
value,
|
|
56
|
+
currency,
|
|
57
|
+
properties,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof ModelValidationError) {
|
|
62
|
+
return response.json({
|
|
63
|
+
message: 'Validation failed.',
|
|
64
|
+
errors: error.errors,
|
|
65
|
+
}, 422)
|
|
66
|
+
}
|
|
67
|
+
return dashboardOperationalError(error, 'Analytics event could not be recorded.', 'EventStoreAction', 500)
|
|
68
|
+
}
|
|
57
69
|
|
|
58
70
|
return response.json({ success: true }, 201)
|
|
59
71
|
},
|
|
@@ -2,6 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
|
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { products } from '@stacksjs/commerce'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
6
|
|
|
6
7
|
export default new Action({
|
|
7
8
|
name: 'ProductUnitDefaultAction',
|
|
@@ -11,11 +12,19 @@ export default new Action({
|
|
|
11
12
|
|
|
12
13
|
async handle(request: RequestInstance) {
|
|
13
14
|
const id = Number(request.getParam('id'))
|
|
15
|
+
if (!Number.isSafeInteger(id) || id <= 0)
|
|
16
|
+
return response.json({ error: 'A valid product unit id is required.' }, 400)
|
|
17
|
+
if (typeof request.get('isDefault') !== 'boolean')
|
|
18
|
+
return response.json({ error: 'isDefault must be a boolean.' }, 422)
|
|
14
19
|
const isDefault = request.boolean('isDefault')
|
|
15
|
-
if (!Number.isFinite(id) || id <= 0)
|
|
16
|
-
return response.notFound({ error: 'Product unit not found' })
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
let updated
|
|
22
|
+
try {
|
|
23
|
+
updated = await products.units.updateDefaultStatus(id, isDefault)
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return dashboardOperationalError(error, 'Product unit default could not be updated.', 'ProductUnitDefaultAction', 500)
|
|
27
|
+
}
|
|
19
28
|
if (!updated)
|
|
20
29
|
return response.notFound({ error: 'Product unit not found' })
|
|
21
30
|
|
|
@@ -2,6 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
|
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { tax } from '@stacksjs/commerce'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
6
|
|
|
6
7
|
export default new Action({
|
|
7
8
|
name: 'TaxRateDefaultAction',
|
|
@@ -11,11 +12,19 @@ export default new Action({
|
|
|
11
12
|
|
|
12
13
|
async handle(request: RequestInstance) {
|
|
13
14
|
const id = Number(request.getParam('id'))
|
|
15
|
+
if (!Number.isSafeInteger(id) || id <= 0)
|
|
16
|
+
return response.json({ error: 'A valid tax rate id is required.' }, 400)
|
|
17
|
+
if (typeof request.get('isDefault') !== 'boolean')
|
|
18
|
+
return response.json({ error: 'isDefault must be a boolean.' }, 422)
|
|
14
19
|
const isDefault = request.boolean('isDefault')
|
|
15
|
-
if (!Number.isFinite(id) || id <= 0)
|
|
16
|
-
return response.notFound({ error: 'Tax rate not found' })
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
let updated
|
|
22
|
+
try {
|
|
23
|
+
updated = await tax.updateDefaultStatus(id, isDefault)
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return dashboardOperationalError(error, 'Tax rate default could not be updated.', 'TaxRateDefaultAction', 500)
|
|
27
|
+
}
|
|
19
28
|
if (!updated)
|
|
20
29
|
return response.notFound({ error: 'Tax rate not found' })
|
|
21
30
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
|
+
import { transaction } from '@stacksjs/orm'
|
|
4
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
7
|
import { rowExists, rowId, timestamp } from './content-input'
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -22,17 +24,29 @@ export default new Action({
|
|
|
22
24
|
if (!id)
|
|
23
25
|
return response.json({ message: 'A valid author id is required.' }, 422)
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
try {
|
|
28
|
+
const deleted = await transaction(async (rawTrx) => {
|
|
29
|
+
const trx = rawTrx as unknown as typeof db
|
|
30
|
+
if (!await rowExists('authors', id, trx))
|
|
31
|
+
return false
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
await trx
|
|
34
|
+
.updateTable('posts')
|
|
35
|
+
.set({ author_id: null, updated_at: timestamp() } as any)
|
|
36
|
+
.where('author_id', '=', id)
|
|
37
|
+
.execute()
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
await trx.deleteFrom('authors').where('id', '=', id).execute()
|
|
40
|
+
return true
|
|
41
|
+
})
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
if (!deleted)
|
|
44
|
+
return response.json({ message: 'Author not found.' }, 404)
|
|
45
|
+
|
|
46
|
+
return response.json({ message: 'Author deleted.', id })
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return dashboardOperationalError(error, 'Author could not be deleted.', 'AuthorDestroyAction', 500)
|
|
50
|
+
}
|
|
37
51
|
},
|
|
38
52
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { db } from '@stacksjs/database'
|
|
3
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
3
4
|
|
|
4
5
|
interface AuthorRow {
|
|
5
6
|
id: number
|
|
@@ -30,36 +31,41 @@ export default new Action({
|
|
|
30
31
|
method: 'GET',
|
|
31
32
|
apiResponse: true,
|
|
32
33
|
async handle() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
try {
|
|
35
|
+
const [rows, postAuthors] = await Promise.all([
|
|
36
|
+
db.selectFrom('authors').selectAll().orderBy('created_at', 'desc').execute() as unknown as Promise<AuthorRow[]>,
|
|
37
|
+
db.selectFrom('posts').select(['author_id']).execute() as unknown as Promise<Array<{ author_id: number | null }>>,
|
|
38
|
+
])
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const postCounts = new Map<number, number>()
|
|
41
|
+
for (const post of postAuthors) {
|
|
42
|
+
if (post.author_id == null)
|
|
43
|
+
continue
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const id = Number(post.author_id)
|
|
46
|
+
postCounts.set(id, (postCounts.get(id) || 0) + 1)
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const authors = rows.map(row => ({
|
|
50
|
+
id: Number(row.id),
|
|
51
|
+
name: String(row.name || ''),
|
|
52
|
+
email: String(row.email || ''),
|
|
53
|
+
bio: String(row.bio || ''),
|
|
54
|
+
avatar: String(row.avatar || ''),
|
|
55
|
+
postCount: postCounts.get(Number(row.id)) || 0,
|
|
56
|
+
created_at: row.created_at || null,
|
|
57
|
+
updated_at: row.updated_at || null,
|
|
58
|
+
}))
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Posts whose author no longer exists are not counted: the stat means
|
|
61
|
+
// "posts reachable from this list", which is what the page's Assigned Posts
|
|
62
|
+
// tile claims to show.
|
|
63
|
+
const assignedPostCount = authors.reduce((sum, author) => sum + author.postCount, 0)
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
return { authors, assignedPostCount }
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return dashboardOperationalError(error, 'Authors could not be loaded.', 'AuthorIndexAction')
|
|
69
|
+
}
|
|
64
70
|
},
|
|
65
71
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
|
+
import { isUniqueViolation, transaction } from '@stacksjs/orm'
|
|
4
5
|
import { response } from '@stacksjs/router'
|
|
5
6
|
import { randomUUIDv7 } from 'bun'
|
|
7
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
8
|
import { parseAuthorInput } from './author-input'
|
|
7
9
|
import { findRow, insertedId, timestamp } from './content-input'
|
|
8
10
|
|
|
@@ -25,35 +27,53 @@ export default new Action({
|
|
|
25
27
|
if ('message' in input)
|
|
26
28
|
return response.json({ message: input.message }, 422)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
try {
|
|
31
|
+
const author = await transaction(async (rawTrx) => {
|
|
32
|
+
const trx = rawTrx as unknown as typeof db
|
|
33
|
+
const duplicate = await trx
|
|
34
|
+
.selectFrom('authors')
|
|
35
|
+
.select(['id'])
|
|
36
|
+
.where('email', '=', input.data.email)
|
|
37
|
+
.executeTakeFirst()
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if (duplicate)
|
|
40
|
+
return null
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
const now = timestamp()
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
const result = await trx
|
|
45
|
+
.insertInto('authors')
|
|
46
|
+
.values({
|
|
47
|
+
uuid: randomUUIDv7(),
|
|
48
|
+
name: input.data.name,
|
|
49
|
+
email: input.data.email,
|
|
50
|
+
bio: input.data.bio || null,
|
|
51
|
+
avatar: input.data.avatar || null,
|
|
52
|
+
created_at: now,
|
|
53
|
+
updated_at: now,
|
|
54
|
+
} as any)
|
|
55
|
+
.executeTakeFirst()
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
const id = insertedId(result)
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
if (!id)
|
|
60
|
+
throw new Error('Author insert did not return an id.')
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
const created = await findRow('authors', id, trx)
|
|
63
|
+
if (!created)
|
|
64
|
+
throw new Error('Created author could not be loaded.')
|
|
65
|
+
return created
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (!author)
|
|
69
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
70
|
+
|
|
71
|
+
return response.json(author, 201)
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isUniqueViolation(error))
|
|
75
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
76
|
+
return dashboardOperationalError(error, 'Author could not be created.', 'AuthorStoreAction', 500)
|
|
77
|
+
}
|
|
58
78
|
},
|
|
59
79
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
|
+
import { isUniqueViolation, transaction } from '@stacksjs/orm'
|
|
4
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
7
|
import { parseAuthorInput } from './author-input'
|
|
6
8
|
import { findRow, rowExists, rowId, timestamp } from './content-input'
|
|
7
9
|
|
|
@@ -15,36 +17,56 @@ export default new Action({
|
|
|
15
17
|
if (!id)
|
|
16
18
|
return response.json({ message: 'A valid author id is required.' }, 422)
|
|
17
19
|
|
|
18
|
-
if (!await rowExists('authors', id))
|
|
19
|
-
return response.json({ message: 'Author not found.' }, 404)
|
|
20
|
-
|
|
21
20
|
const input = parseAuthorInput(request)
|
|
22
21
|
|
|
23
22
|
if ('message' in input)
|
|
24
23
|
return response.json({ message: input.message }, 422)
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
25
|
+
try {
|
|
26
|
+
const result = await transaction(async (rawTrx) => {
|
|
27
|
+
const trx = rawTrx as unknown as typeof db
|
|
28
|
+
if (!await rowExists('authors', id, trx))
|
|
29
|
+
return { kind: 'not-found' } as const
|
|
30
|
+
|
|
31
|
+
const duplicate = await trx
|
|
32
|
+
.selectFrom('authors')
|
|
33
|
+
.select(['id'])
|
|
34
|
+
.where('email', '=', input.data.email)
|
|
35
|
+
.where('id', '!=', id)
|
|
36
|
+
.executeTakeFirst()
|
|
37
|
+
|
|
38
|
+
if (duplicate)
|
|
39
|
+
return { kind: 'duplicate' } as const
|
|
40
|
+
|
|
41
|
+
await trx
|
|
42
|
+
.updateTable('authors')
|
|
43
|
+
.set({
|
|
44
|
+
name: input.data.name,
|
|
45
|
+
email: input.data.email,
|
|
46
|
+
bio: input.data.bio || null,
|
|
47
|
+
avatar: input.data.avatar || null,
|
|
48
|
+
updated_at: timestamp(),
|
|
49
|
+
} as any)
|
|
50
|
+
.where('id', '=', id)
|
|
51
|
+
.execute()
|
|
52
|
+
|
|
53
|
+
const author = await findRow('authors', id, trx)
|
|
54
|
+
if (!author)
|
|
55
|
+
throw new Error('Updated author could not be loaded.')
|
|
56
|
+
return { kind: 'updated', author } as const
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (result.kind === 'not-found')
|
|
60
|
+
return response.json({ message: 'Author not found.' }, 404)
|
|
61
|
+
if (result.kind === 'duplicate')
|
|
62
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
63
|
+
|
|
64
|
+
return response.json(result.author)
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (isUniqueViolation(error))
|
|
68
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
69
|
+
return dashboardOperationalError(error, 'Author could not be updated.', 'AuthorUpdateAction', 500)
|
|
70
|
+
}
|
|
49
71
|
},
|
|
50
72
|
})
|