@stacksjs/defaults 0.70.355 → 0.70.356
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-email/SKILL.md +38 -1
- package/app/Actions/ContactAction.ts +2 -2
- package/app/Actions/Dashboard/Analytics/EventAnalyticsAction.ts +2 -3
- package/app/Actions/Dashboard/Analytics/MarketingAnalyticsAction.ts +2 -3
- package/app/Actions/Dashboard/Analytics/SalesAnalyticsAction.ts +2 -3
- package/app/Actions/Dashboard/Analytics/WebAnalyticsAction.ts +9 -3
- package/app/Actions/Dashboard/Auth/MeAction.ts +4 -7
- package/app/Actions/Dashboard/Buddy/BuddyChatAction.ts +2 -3
- package/app/Actions/Dashboard/Cloud/CloudIndexAction.ts +8 -1
- package/app/Actions/Dashboard/Cloud/ServerlessIndexAction.ts +15 -8
- package/app/Actions/Dashboard/Commerce/CommerceCategoriesAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceCouponsAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceGiftCardsAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommercePaymentsAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +75 -61
- package/app/Actions/Dashboard/Commerce/CommercePrintDevicesAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +3 -4
- package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/CommerceTaxesAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/DigitalDeliveryIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/LicenseKeyOptionsAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ManufacturerIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ProductUnitIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ProductVariantIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ProductWaitlistIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/RestaurantWaitlistIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ReviewIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +2 -4
- package/app/Actions/Dashboard/DashboardActivityAction.ts +2 -5
- package/app/Actions/Dashboard/Data/ActivityIndexAction.ts +2 -5
- package/app/Actions/Dashboard/Data/SubscriberIndexAction.ts +2 -5
- package/app/Actions/Dashboard/Data/TeamIndexAction.ts +2 -5
- package/app/Actions/Dashboard/Data/UserIndexAction.ts +2 -5
- package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +23 -19
- package/app/Actions/Dashboard/Deployments/GetAverageDeploymentTime.ts +8 -2
- package/app/Actions/Dashboard/Deployments/GetDeployScript.ts +3 -6
- package/app/Actions/Dashboard/Deployments/GetDeployment.ts +4 -5
- package/app/Actions/Dashboard/Deployments/GetDeploymentCount.ts +7 -1
- package/app/Actions/Dashboard/Deployments/GetDeploymentLiveTerminalOutput.ts +10 -9
- package/app/Actions/Dashboard/Deployments/GetDeployments.ts +2 -4
- package/app/Actions/Dashboard/Deployments/GetRecentDeployments.ts +8 -2
- package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +18 -5
- package/app/Actions/Dashboard/Email/CapturedMailIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Email/CapturedMailShowAction.ts +3 -4
- package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +15 -14
- package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +2 -8
- package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +2 -4
- package/app/Actions/Dashboard/Jobs/JobShowAction.ts +13 -8
- package/app/Actions/Dashboard/Jobs/JobStatsAction.ts +2 -4
- package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardShowAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/UsersListAction.ts +2 -3
- package/app/Actions/Dashboard/Kanban/kanban-response.ts +5 -0
- package/app/Actions/Dashboard/Library/CreateComponent.ts +15 -6
- package/app/Actions/Dashboard/Library/CreateFunction.ts +15 -6
- package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +88 -77
- package/app/Actions/Dashboard/Library/GetAverageReleaseTime.ts +21 -15
- package/app/Actions/Dashboard/Library/GetComponents.ts +14 -7
- package/app/Actions/Dashboard/Library/GetDownloadCount.ts +14 -5
- package/app/Actions/Dashboard/Library/GetFunctions.ts +12 -5
- package/app/Actions/Dashboard/Library/GetReleaseCount.ts +14 -5
- package/app/Actions/Dashboard/Library/GetReleases.ts +17 -10
- package/app/Actions/Dashboard/Library/PackageIndexAction.ts +14 -7
- package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +7 -5
- package/app/Actions/Dashboard/Models/ModelShowAction.ts +82 -174
- package/app/Actions/Dashboard/Models/ModelStoreAction.ts +6 -4
- package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -7
- package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +47 -17
- package/app/Actions/Dashboard/Models/model-write.ts +20 -5
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +46 -36
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +14 -6
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction.ts +96 -84
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryRetryAction.ts +34 -21
- package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +2 -4
- package/app/Actions/Dashboard/Queries/QueryShowAction.ts +2 -3
- package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +2 -4
- package/app/Actions/Dashboard/Queue/QueueWorkersAction.ts +2 -4
- package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +2 -2
- package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +2 -2
- package/app/Actions/Dashboard/Rbac/PermissionsIndexAction.ts +2 -3
- package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +2 -2
- package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +2 -2
- package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +2 -6
- package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +2 -2
- package/app/Actions/Dashboard/Rbac/RolesIndexAction.ts +4 -6
- package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +2 -2
- package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +2 -9
- package/app/Actions/Dashboard/Rbac/UsersListAction.ts +2 -3
- package/app/Actions/Dashboard/Rbac/rbac-response.ts +10 -0
- package/app/Actions/Dashboard/Realtime/RealtimeStatsAction.ts +2 -4
- package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +68 -55
- package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +34 -16
- package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +52 -29
- package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +102 -71
- package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +36 -17
- package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +39 -26
- package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +71 -58
- package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +29 -7
- package/app/Actions/Dashboard/Teams/team-member-count.ts +4 -2
- package/app/Actions/Dashboard/Teams/team-records.test.ts +2 -0
- package/app/Actions/Dashboard/Teams/team-records.ts +2 -0
- package/app/Actions/Dashboard/dashboard-response.ts +16 -0
- package/app/Actions/SendWelcomeEmail.ts +1 -1
- package/app/Actions/Teams/AcceptInvitationAction.ts +74 -51
- package/app/Actions/Teams/ShowInvitationAction.ts +21 -14
- package/app/Actions/Teams/team-response.ts +23 -0
- package/app/Mail/OrderConfirmation.ts +1 -1
- package/app/Mail/SubscriptionConfirmation.ts +1 -1
- package/app/Mail/TeamInvitation.ts +1 -1
- package/app/Models/EmailIdempotency.ts +64 -0
- package/app/Models/EmailSuppression.ts +52 -0
- package/app/Models/EmailWebhookEvent.ts +51 -0
- package/app/Models/TeamInvitation.ts +13 -0
- package/functions/data.ts +0 -1
- package/functions/jobs.ts +0 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Data/DataRecords.stx +0 -4
- package/resources/components/Dashboard/Jobs/JobDetail.stx +3 -2
- package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +7 -13
- package/resources/components/Dashboard/Models/ModelsOverview.stx +1 -1
- package/views/dashboard/.discovered-models.json +30 -0
- package/views/dashboard/stores/auth.ts +15 -14
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
sqlTimestamp,
|
|
14
14
|
} from '../Dashboard/Teams/team-records'
|
|
15
15
|
import { syncTeamMemberCount } from '../Dashboard/Teams/team-member-count'
|
|
16
|
+
import { teamOperationalError } from './team-response'
|
|
16
17
|
|
|
17
18
|
export default new Action({
|
|
18
19
|
name: 'Accept Team Invitation',
|
|
@@ -25,23 +26,38 @@ export default new Action({
|
|
|
25
26
|
if (!token || token.length > 200)
|
|
26
27
|
return response.json({ message: 'Invitation not found.' }, 404)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
let authenticated
|
|
30
|
+
try {
|
|
31
|
+
authenticated = await resolveAuthenticatedUser(request as any)
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return teamOperationalError(error, 'Authentication could not be verified.', 'AcceptInvitationAction.auth')
|
|
35
|
+
}
|
|
29
36
|
if (!authenticated?.id)
|
|
30
37
|
return response.json({ message: 'Sign in to accept this invitation.' }, 401)
|
|
31
38
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
39
|
+
const tokenHash = hashInvitationToken(token)
|
|
40
|
+
let user
|
|
41
|
+
let invitation
|
|
42
|
+
try {
|
|
43
|
+
[user, invitation] = await Promise.all([
|
|
44
|
+
db
|
|
45
|
+
.selectFrom('users')
|
|
46
|
+
.where('id', '=', authenticated.id)
|
|
47
|
+
.select(['id', 'email'])
|
|
48
|
+
.executeTakeFirst(),
|
|
49
|
+
(db as any)
|
|
50
|
+
.selectFrom('team_invitations')
|
|
51
|
+
.where('token_hash', '=', tokenHash)
|
|
52
|
+
.select(['id', 'team_id', 'email', 'role', 'status', 'expires_at'])
|
|
53
|
+
.executeTakeFirst(),
|
|
54
|
+
])
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return teamOperationalError(error, 'Invitation acceptance could not be prepared.', 'AcceptInvitationAction.lookup')
|
|
58
|
+
}
|
|
37
59
|
if (!user)
|
|
38
60
|
return response.json({ message: 'Authenticated user not found.' }, 401)
|
|
39
|
-
|
|
40
|
-
const invitation = await (db as any)
|
|
41
|
-
.selectFrom('team_invitations')
|
|
42
|
-
.where('token_hash', '=', hashInvitationToken(token))
|
|
43
|
-
.select(['id', 'team_id', 'email', 'role', 'status', 'expires_at'])
|
|
44
|
-
.executeTakeFirst()
|
|
45
61
|
if (!invitation)
|
|
46
62
|
return response.json({ message: 'Invitation not found.' }, 404)
|
|
47
63
|
|
|
@@ -58,49 +74,56 @@ export default new Action({
|
|
|
58
74
|
const userId = Number(user.id)
|
|
59
75
|
const now = sqlTimestamp()
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
let accepted
|
|
78
|
+
try {
|
|
79
|
+
accepted = await db.transaction(async (rawTrx) => {
|
|
80
|
+
const trx = rawTrx as unknown as typeof db
|
|
81
|
+
const claim = await (trx as any)
|
|
82
|
+
.updateTable('team_invitations')
|
|
83
|
+
.set({
|
|
84
|
+
status: 'accepted',
|
|
85
|
+
token_hash: hashInvitationToken(generateInvitationToken()),
|
|
86
|
+
pending_key: null,
|
|
87
|
+
accepted_by_user_id: userId,
|
|
88
|
+
accepted_at: now,
|
|
89
|
+
updated_at: now,
|
|
90
|
+
})
|
|
91
|
+
.where('id', '=', Number(invitation.id))
|
|
92
|
+
.where('token_hash', '=', tokenHash)
|
|
93
|
+
.where('status', '=', 'pending')
|
|
94
|
+
.executeTakeFirst()
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
if (changedRows(claim) !== 1)
|
|
97
|
+
return null
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
const existing = await (trx as any)
|
|
100
|
+
.selectFrom('team_members')
|
|
101
|
+
.where('team_id', '=', teamId)
|
|
102
|
+
.where('user_id', '=', userId)
|
|
103
|
+
.select(['id'])
|
|
104
|
+
.executeTakeFirst()
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
if (!existing) {
|
|
107
|
+
await (trx as any)
|
|
108
|
+
.insertInto('team_members')
|
|
109
|
+
.values({
|
|
110
|
+
team_id: teamId,
|
|
111
|
+
user_id: userId,
|
|
112
|
+
role: String(invitation.role),
|
|
113
|
+
status: 'active',
|
|
114
|
+
created_at: now,
|
|
115
|
+
uuid: randomUUID(),
|
|
116
|
+
})
|
|
117
|
+
.execute()
|
|
118
|
+
}
|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
await syncTeamMemberCount(teamId, trx)
|
|
121
|
+
return { alreadyMember: Boolean(existing) }
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return teamOperationalError(error, 'The invitation could not be accepted.', 'AcceptInvitationAction.accept', 500)
|
|
126
|
+
}
|
|
104
127
|
|
|
105
128
|
if (!accepted)
|
|
106
129
|
return response.json({ message: 'This invitation is no longer available.' }, 409)
|
|
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
5
|
import { hashInvitationToken, invitationStatus } from '../Dashboard/Teams/team-records'
|
|
6
|
+
import { teamOperationalError } from './team-response'
|
|
6
7
|
|
|
7
8
|
export default new Action({
|
|
8
9
|
name: 'Show Team Invitation',
|
|
@@ -15,20 +16,26 @@ export default new Action({
|
|
|
15
16
|
if (!token || token.length > 200)
|
|
16
17
|
return response.json({ message: 'Invitation not found.' }, 404)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'team_invitations.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
let invitation
|
|
20
|
+
try {
|
|
21
|
+
invitation = await (db as any)
|
|
22
|
+
.selectFrom('team_invitations')
|
|
23
|
+
.innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
|
|
24
|
+
.where('team_invitations.token_hash', '=', hashInvitationToken(token))
|
|
25
|
+
.select([
|
|
26
|
+
'team_invitations.id as id',
|
|
27
|
+
'team_invitations.team_id as team_id',
|
|
28
|
+
'team_invitations.email as email',
|
|
29
|
+
'team_invitations.role as role',
|
|
30
|
+
'team_invitations.status as status',
|
|
31
|
+
'team_invitations.expires_at as expires_at',
|
|
32
|
+
'teams.name as team_name',
|
|
33
|
+
])
|
|
34
|
+
.executeTakeFirst()
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return teamOperationalError(error, 'Invitation details could not be loaded.', 'ShowInvitationAction')
|
|
38
|
+
}
|
|
32
39
|
if (!invitation)
|
|
33
40
|
return response.json({ message: 'Invitation not found.' }, 404)
|
|
34
41
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { response } from '@stacksjs/router'
|
|
2
|
+
|
|
3
|
+
export class TeamStateConflictError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message)
|
|
6
|
+
this.name = 'TeamStateConflictError'
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function teamOperationalError(
|
|
11
|
+
error: unknown,
|
|
12
|
+
message: string,
|
|
13
|
+
action: string,
|
|
14
|
+
status = 503,
|
|
15
|
+
): Response {
|
|
16
|
+
console.error(`[teams/api] ${action} failed:`, error)
|
|
17
|
+
return response.json({ message }, status)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function teamOperationalIssue(error: unknown, message: string, action: string): string {
|
|
21
|
+
console.error(`[teams/api] ${action} failed:`, error)
|
|
22
|
+
return message
|
|
23
|
+
}
|
|
@@ -48,7 +48,7 @@ export async function sendOrderConfirmation(options: OrderConfirmationOptions):
|
|
|
48
48
|
subject: `Your ${appName} order #${options.orderId} is confirmed`,
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
await mail.
|
|
51
|
+
await mail.sendOrFail({
|
|
52
52
|
to: [options.to],
|
|
53
53
|
from: { name: appName, address: fromAddress },
|
|
54
54
|
subject: `Your ${appName} order #${options.orderId} is confirmed`,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
export default defineModel({
|
|
5
|
+
name: 'EmailIdempotency',
|
|
6
|
+
table: 'email_idempotency',
|
|
7
|
+
primaryKey: 'id',
|
|
8
|
+
autoIncrement: true,
|
|
9
|
+
|
|
10
|
+
traits: {
|
|
11
|
+
useTimestamps: true,
|
|
12
|
+
useApi: {
|
|
13
|
+
uri: 'email-idempotency',
|
|
14
|
+
routes: ['index', 'show', 'destroy'],
|
|
15
|
+
middleware: ['auth'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
attributes: {
|
|
20
|
+
idempotencyKey: {
|
|
21
|
+
required: true,
|
|
22
|
+
hidden: true,
|
|
23
|
+
unique: true,
|
|
24
|
+
validation: {
|
|
25
|
+
rule: schema.string().required().max(255),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
messageId: {
|
|
29
|
+
required: false,
|
|
30
|
+
validation: {
|
|
31
|
+
rule: schema.string().max(512),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
recipient: {
|
|
35
|
+
required: false,
|
|
36
|
+
hidden: true,
|
|
37
|
+
validation: {
|
|
38
|
+
rule: schema.string().max(2000),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
subject: {
|
|
42
|
+
required: false,
|
|
43
|
+
hidden: true,
|
|
44
|
+
validation: {
|
|
45
|
+
rule: schema.string().max(998),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
provider: {
|
|
49
|
+
required: false,
|
|
50
|
+
validation: {
|
|
51
|
+
rule: schema.string().max(100),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
success: {
|
|
55
|
+
required: true,
|
|
56
|
+
default: true,
|
|
57
|
+
validation: {
|
|
58
|
+
rule: schema.boolean(),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
dashboard: { enabled: false },
|
|
64
|
+
} as const)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
export default defineModel({
|
|
5
|
+
name: 'EmailSuppression',
|
|
6
|
+
table: 'email_suppressions',
|
|
7
|
+
primaryKey: 'id',
|
|
8
|
+
autoIncrement: true,
|
|
9
|
+
|
|
10
|
+
indexes: [
|
|
11
|
+
{
|
|
12
|
+
name: 'email_suppressions_email_type_unique',
|
|
13
|
+
columns: ['email', 'type'],
|
|
14
|
+
unique: true,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
traits: {
|
|
19
|
+
useTimestamps: true,
|
|
20
|
+
useApi: {
|
|
21
|
+
uri: 'email-suppressions',
|
|
22
|
+
routes: ['index', 'show', 'destroy'],
|
|
23
|
+
middleware: ['auth'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
attributes: {
|
|
28
|
+
email: {
|
|
29
|
+
required: true,
|
|
30
|
+
fillable: true,
|
|
31
|
+
validation: {
|
|
32
|
+
rule: schema.string().email().required().max(320),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
type: {
|
|
36
|
+
required: true,
|
|
37
|
+
fillable: true,
|
|
38
|
+
validation: {
|
|
39
|
+
rule: schema.enum(['bounce', 'complaint', 'unsubscribe', 'manual']),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
reason: {
|
|
43
|
+
required: false,
|
|
44
|
+
fillable: true,
|
|
45
|
+
validation: {
|
|
46
|
+
rule: schema.string().max(2000),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
dashboard: { enabled: false },
|
|
52
|
+
} as const)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
export default defineModel({
|
|
5
|
+
name: 'EmailWebhookEvent',
|
|
6
|
+
table: 'email_webhook_events',
|
|
7
|
+
primaryKey: 'id',
|
|
8
|
+
autoIncrement: true,
|
|
9
|
+
|
|
10
|
+
indexes: [
|
|
11
|
+
{
|
|
12
|
+
name: 'email_webhook_events_provider_event_unique',
|
|
13
|
+
columns: ['provider', 'event_id'],
|
|
14
|
+
unique: true,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
traits: {
|
|
19
|
+
useTimestamps: true,
|
|
20
|
+
useApi: {
|
|
21
|
+
uri: 'email-webhook-events',
|
|
22
|
+
routes: ['index', 'show', 'destroy'],
|
|
23
|
+
middleware: ['auth'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
attributes: {
|
|
28
|
+
provider: {
|
|
29
|
+
required: true,
|
|
30
|
+
fillable: true,
|
|
31
|
+
validation: {
|
|
32
|
+
rule: schema.enum(['mailgun', 'postmark', 'ses', 'sendgrid']),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
eventId: {
|
|
36
|
+
required: true,
|
|
37
|
+
hidden: true,
|
|
38
|
+
validation: {
|
|
39
|
+
rule: schema.string().required().max(512),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
processedAt: {
|
|
43
|
+
required: true,
|
|
44
|
+
validation: {
|
|
45
|
+
rule: schema.timestamp().required(),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
dashboard: { enabled: false },
|
|
51
|
+
} as const)
|
|
@@ -20,6 +20,11 @@ export default defineModel({
|
|
|
20
20
|
columns: ['token_hash'],
|
|
21
21
|
unique: true,
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
name: 'team_invitations_pending_key_unique',
|
|
25
|
+
columns: ['pending_key'],
|
|
26
|
+
unique: true,
|
|
27
|
+
},
|
|
23
28
|
{
|
|
24
29
|
name: 'team_invitations_team_email_status_index',
|
|
25
30
|
columns: ['team_id', 'email', 'status'],
|
|
@@ -79,6 +84,14 @@ export default defineModel({
|
|
|
79
84
|
},
|
|
80
85
|
},
|
|
81
86
|
|
|
87
|
+
pendingKey: {
|
|
88
|
+
required: false,
|
|
89
|
+
hidden: true,
|
|
90
|
+
validation: {
|
|
91
|
+
rule: schema.string().max(384),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
82
95
|
invitedByUserId: {
|
|
83
96
|
required: false,
|
|
84
97
|
validation: {
|
package/functions/data.ts
CHANGED
package/functions/jobs.ts
CHANGED
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.356",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -98,25 +98,21 @@ async function loadRecords(): Promise<void> {
|
|
|
98
98
|
const response = await fetchDashboardData<DashboardActivity>('activity')
|
|
99
99
|
records.set(response.activities || [])
|
|
100
100
|
stats.set(response.stats)
|
|
101
|
-
loadError.set(response.error || '')
|
|
102
101
|
}
|
|
103
102
|
else if (mode() === 'users') {
|
|
104
103
|
const response = await fetchDashboardData<DashboardUser>('users')
|
|
105
104
|
records.set(response.users || [])
|
|
106
105
|
stats.set(response.stats)
|
|
107
|
-
loadError.set(response.error || '')
|
|
108
106
|
}
|
|
109
107
|
else if (mode() === 'teams') {
|
|
110
108
|
const response = await fetchDashboardData<DashboardTeam>('teams')
|
|
111
109
|
records.set(response.teams || [])
|
|
112
110
|
stats.set(response.stats)
|
|
113
|
-
loadError.set(response.error || '')
|
|
114
111
|
}
|
|
115
112
|
else {
|
|
116
113
|
const response = await fetchDashboardData<DashboardSubscriber>('subscribers')
|
|
117
114
|
records.set(response.subscribers || [])
|
|
118
115
|
stats.set(response.stats)
|
|
119
|
-
loadError.set(response.error || '')
|
|
120
116
|
}
|
|
121
117
|
}
|
|
122
118
|
catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { DashboardJob } from '../../../../functions/jobs'
|
|
3
|
+
import { DashboardApiError } from '../../../../functions/dashboard-api'
|
|
3
4
|
import { fetchJob, retryJob } from '../../../../functions/jobs'
|
|
4
5
|
import { pushToast } from '../../../../functions/toasts'
|
|
5
6
|
|
|
@@ -53,11 +54,11 @@ async function loadJob(): Promise<void> {
|
|
|
53
54
|
try {
|
|
54
55
|
const response = await fetchJob(jobId())
|
|
55
56
|
job.set(response.job)
|
|
56
|
-
loadError.set(response.error || '')
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
59
59
|
job.set(null)
|
|
60
|
-
|
|
60
|
+
if (!(error instanceof DashboardApiError && error.status === 404))
|
|
61
|
+
loadError.set(error instanceof Error ? error.message : 'Job could not be loaded.')
|
|
61
62
|
}
|
|
62
63
|
finally {
|
|
63
64
|
isLoading.set(false)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// works the same on a table with 10 rows and one with a million. Rows and
|
|
8
8
|
// writes go through the model, so scopes, casts and observers apply.
|
|
9
9
|
import { useNavigation } from '~/storage/framework/defaults/views/dashboard/composables/useNavigation'
|
|
10
|
-
import { dashboardApi } from '~/storage/framework/defaults/functions/dashboard-api'
|
|
10
|
+
import { DashboardApiError, dashboardApi } from '~/storage/framework/defaults/functions/dashboard-api'
|
|
11
11
|
import { pushToast } from '~/storage/framework/defaults/functions/toasts'
|
|
12
12
|
|
|
13
13
|
interface ColumnMeta {
|
|
@@ -46,7 +46,6 @@ interface ResponseShape {
|
|
|
46
46
|
writable: boolean
|
|
47
47
|
writeCapabilities: WriteCapabilities
|
|
48
48
|
createFields: CreateField[]
|
|
49
|
-
source: 'orm' | 'sqlite-fallback'
|
|
50
49
|
error: string | null
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -67,7 +66,6 @@ const search = state(navigation.urlQuery('q') || '')
|
|
|
67
66
|
const filters = state<Record<string, string>>({})
|
|
68
67
|
const writeCapabilities = state<WriteCapabilities>({ create: false, update: false, destroy: false })
|
|
69
68
|
const createFields = state<CreateField[]>([])
|
|
70
|
-
const source = state<'orm' | 'sqlite-fallback'>('orm')
|
|
71
69
|
const error = state<string | null>(null)
|
|
72
70
|
const loading = state(true)
|
|
73
71
|
const showFilters = state(false)
|
|
@@ -86,14 +84,6 @@ const createErrors = state<Record<string, string[]>>({})
|
|
|
86
84
|
|
|
87
85
|
const slug = derived(() => navigation.urlSegment(-1) || '')
|
|
88
86
|
|
|
89
|
-
const sourceLabel = derived(() => {
|
|
90
|
-
if (source() === 'orm')
|
|
91
|
-
return 'via Stacks ORM'
|
|
92
|
-
return Object.values(writeCapabilities()).some(Boolean)
|
|
93
|
-
? 'SQLite query, ORM writes'
|
|
94
|
-
: 'raw SQLite, read-only'
|
|
95
|
-
})
|
|
96
|
-
|
|
97
87
|
const recordLabel = derived(() => {
|
|
98
88
|
const n = totalCount()
|
|
99
89
|
return `${n.toLocaleString()} ${n === 1 ? 'record' : 'records'}`
|
|
@@ -223,7 +213,6 @@ async function load() {
|
|
|
223
213
|
sort.set(data.sort ?? '')
|
|
224
214
|
writeCapabilities.set(data.writeCapabilities ?? { create: false, update: false, destroy: false })
|
|
225
215
|
createFields.set(data.createFields ?? [])
|
|
226
|
-
source.set(data.source ?? 'orm')
|
|
227
216
|
error.set(data.error ?? null)
|
|
228
217
|
}
|
|
229
218
|
catch (e) {
|
|
@@ -319,6 +308,11 @@ async function createRecord(fields: Record<string, unknown>): Promise<void> {
|
|
|
319
308
|
}
|
|
320
309
|
catch (reason) {
|
|
321
310
|
createError.set(reason instanceof Error ? reason.message : String(reason))
|
|
311
|
+
if (reason instanceof DashboardApiError && reason.fields) {
|
|
312
|
+
createErrors.set(Object.fromEntries(
|
|
313
|
+
Object.entries(reason.fields).map(([field, message]) => [field, [message]]),
|
|
314
|
+
))
|
|
315
|
+
}
|
|
322
316
|
}
|
|
323
317
|
finally {
|
|
324
318
|
creating.set(false)
|
|
@@ -402,7 +396,7 @@ void load()
|
|
|
402
396
|
<div class="flex flex-wrap gap-4 items-start justify-between">
|
|
403
397
|
<div class="min-w-0">
|
|
404
398
|
<h1 class="font-semibold text-2xl text-neutral-900 truncate dark:text-white" :text="modelName()"></h1>
|
|
405
|
-
<p class="mt-1 text-[13px] text-neutral-500 dark:text-neutral-400" :text="recordLabel() + ' · ' + tableName() + ' ·
|
|
399
|
+
<p class="mt-1 text-[13px] text-neutral-500 dark:text-neutral-400" :text="recordLabel() + ' · ' + tableName() + ' · via Stacks ORM'"></p>
|
|
406
400
|
</div>
|
|
407
401
|
<div class="flex gap-2 items-center">
|
|
408
402
|
<Button
|
|
@@ -129,7 +129,7 @@ onMount(() => {
|
|
|
129
129
|
|
|
130
130
|
<div :if="!loadError() && unavailableModels() > 0" role="status" class="flex gap-3 items-start px-4 py-3 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 rounded-lg dark:border-amber-900">
|
|
131
131
|
<span class="flex-none mt-0.5 h-4 w-4 i-hugeicons-alert-02"></span>
|
|
132
|
-
<span>{{ unavailableModels() }} model {{ unavailableModels() === 1 ? 'count is' : 'counts are' }} unavailable. Affected cards
|
|
132
|
+
<span>{{ unavailableModels() }} model {{ unavailableModels() === 1 ? 'count is' : 'counts are' }} unavailable. Affected cards remain unavailable instead of reporting zero records.</span>
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
135
|
<Card padding="none">
|
|
@@ -185,6 +185,16 @@
|
|
|
185
185
|
"hasDedicatedPage": false,
|
|
186
186
|
"category": "commerce"
|
|
187
187
|
},
|
|
188
|
+
{
|
|
189
|
+
"id": "email-idempotency",
|
|
190
|
+
"name": "EmailIdempotency",
|
|
191
|
+
"icon": "tablecells.fill",
|
|
192
|
+
"hasDedicatedPage": false,
|
|
193
|
+
"category": "data",
|
|
194
|
+
"dashboard": {
|
|
195
|
+
"enabled": false
|
|
196
|
+
}
|
|
197
|
+
},
|
|
188
198
|
{
|
|
189
199
|
"id": "email-list",
|
|
190
200
|
"name": "EmailList",
|
|
@@ -199,6 +209,26 @@
|
|
|
199
209
|
"hasDedicatedPage": false,
|
|
200
210
|
"category": "data"
|
|
201
211
|
},
|
|
212
|
+
{
|
|
213
|
+
"id": "email-suppression",
|
|
214
|
+
"name": "EmailSuppression",
|
|
215
|
+
"icon": "tablecells.fill",
|
|
216
|
+
"hasDedicatedPage": false,
|
|
217
|
+
"category": "data",
|
|
218
|
+
"dashboard": {
|
|
219
|
+
"enabled": false
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"id": "email-webhook-event",
|
|
224
|
+
"name": "EmailWebhookEvent",
|
|
225
|
+
"icon": "tablecells.fill",
|
|
226
|
+
"hasDedicatedPage": false,
|
|
227
|
+
"category": "data",
|
|
228
|
+
"dashboard": {
|
|
229
|
+
"enabled": false
|
|
230
|
+
}
|
|
231
|
+
},
|
|
202
232
|
{
|
|
203
233
|
"id": "gift-card",
|
|
204
234
|
"name": "GiftCard",
|