@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.
Files changed (155) hide show
  1. package/ai/skills/stacks-email/SKILL.md +38 -1
  2. package/app/Actions/ContactAction.ts +2 -2
  3. package/app/Actions/Dashboard/Analytics/EventAnalyticsAction.ts +2 -3
  4. package/app/Actions/Dashboard/Analytics/MarketingAnalyticsAction.ts +2 -3
  5. package/app/Actions/Dashboard/Analytics/SalesAnalyticsAction.ts +2 -3
  6. package/app/Actions/Dashboard/Analytics/WebAnalyticsAction.ts +9 -3
  7. package/app/Actions/Dashboard/Auth/MeAction.ts +4 -7
  8. package/app/Actions/Dashboard/Buddy/BuddyChatAction.ts +2 -3
  9. package/app/Actions/Dashboard/Cloud/CloudIndexAction.ts +8 -1
  10. package/app/Actions/Dashboard/Cloud/ServerlessIndexAction.ts +15 -8
  11. package/app/Actions/Dashboard/Commerce/CommerceCategoriesAction.ts +2 -4
  12. package/app/Actions/Dashboard/Commerce/CommerceCouponsAction.ts +2 -4
  13. package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +2 -4
  14. package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +2 -4
  15. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +2 -4
  16. package/app/Actions/Dashboard/Commerce/CommerceGiftCardsAction.ts +2 -4
  17. package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +2 -4
  18. package/app/Actions/Dashboard/Commerce/CommercePaymentsAction.ts +2 -4
  19. package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +2 -4
  20. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +75 -61
  21. package/app/Actions/Dashboard/Commerce/CommercePrintDevicesAction.ts +2 -4
  22. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +2 -4
  23. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +3 -4
  24. package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +2 -4
  25. package/app/Actions/Dashboard/Commerce/CommerceTaxesAction.ts +2 -4
  26. package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +2 -4
  27. package/app/Actions/Dashboard/Commerce/DigitalDeliveryIndexAction.ts +2 -4
  28. package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +2 -4
  29. package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +2 -4
  30. package/app/Actions/Dashboard/Commerce/LicenseKeyOptionsAction.ts +2 -4
  31. package/app/Actions/Dashboard/Commerce/ManufacturerIndexAction.ts +2 -4
  32. package/app/Actions/Dashboard/Commerce/ProductUnitIndexAction.ts +2 -4
  33. package/app/Actions/Dashboard/Commerce/ProductVariantIndexAction.ts +2 -4
  34. package/app/Actions/Dashboard/Commerce/ProductWaitlistIndexAction.ts +2 -4
  35. package/app/Actions/Dashboard/Commerce/RestaurantWaitlistIndexAction.ts +2 -4
  36. package/app/Actions/Dashboard/Commerce/ReviewIndexAction.ts +2 -4
  37. package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +2 -4
  38. package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -4
  39. package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +2 -4
  40. package/app/Actions/Dashboard/DashboardActivityAction.ts +2 -5
  41. package/app/Actions/Dashboard/Data/ActivityIndexAction.ts +2 -5
  42. package/app/Actions/Dashboard/Data/SubscriberIndexAction.ts +2 -5
  43. package/app/Actions/Dashboard/Data/TeamIndexAction.ts +2 -5
  44. package/app/Actions/Dashboard/Data/UserIndexAction.ts +2 -5
  45. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +23 -19
  46. package/app/Actions/Dashboard/Deployments/GetAverageDeploymentTime.ts +8 -2
  47. package/app/Actions/Dashboard/Deployments/GetDeployScript.ts +3 -6
  48. package/app/Actions/Dashboard/Deployments/GetDeployment.ts +4 -5
  49. package/app/Actions/Dashboard/Deployments/GetDeploymentCount.ts +7 -1
  50. package/app/Actions/Dashboard/Deployments/GetDeploymentLiveTerminalOutput.ts +10 -9
  51. package/app/Actions/Dashboard/Deployments/GetDeployments.ts +2 -4
  52. package/app/Actions/Dashboard/Deployments/GetRecentDeployments.ts +8 -2
  53. package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +18 -5
  54. package/app/Actions/Dashboard/Email/CapturedMailIndexAction.ts +2 -4
  55. package/app/Actions/Dashboard/Email/CapturedMailShowAction.ts +3 -4
  56. package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +15 -14
  57. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +2 -8
  58. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +2 -4
  59. package/app/Actions/Dashboard/Jobs/JobShowAction.ts +13 -8
  60. package/app/Actions/Dashboard/Jobs/JobStatsAction.ts +2 -4
  61. package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +2 -3
  62. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +2 -3
  63. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +2 -3
  64. package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +2 -3
  65. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +2 -3
  66. package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +2 -3
  67. package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +2 -3
  68. package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +2 -3
  69. package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +2 -3
  70. package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +2 -3
  71. package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +2 -3
  72. package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +2 -3
  73. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +2 -3
  74. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +2 -3
  75. package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +2 -3
  76. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +2 -3
  77. package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +2 -3
  78. package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +2 -3
  79. package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +2 -3
  80. package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +2 -3
  81. package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +2 -3
  82. package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +2 -3
  83. package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +2 -3
  84. package/app/Actions/Dashboard/Kanban/UsersListAction.ts +2 -3
  85. package/app/Actions/Dashboard/Kanban/kanban-response.ts +5 -0
  86. package/app/Actions/Dashboard/Library/CreateComponent.ts +15 -6
  87. package/app/Actions/Dashboard/Library/CreateFunction.ts +15 -6
  88. package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +88 -77
  89. package/app/Actions/Dashboard/Library/GetAverageReleaseTime.ts +21 -15
  90. package/app/Actions/Dashboard/Library/GetComponents.ts +14 -7
  91. package/app/Actions/Dashboard/Library/GetDownloadCount.ts +14 -5
  92. package/app/Actions/Dashboard/Library/GetFunctions.ts +12 -5
  93. package/app/Actions/Dashboard/Library/GetReleaseCount.ts +14 -5
  94. package/app/Actions/Dashboard/Library/GetReleases.ts +17 -10
  95. package/app/Actions/Dashboard/Library/PackageIndexAction.ts +14 -7
  96. package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +7 -5
  97. package/app/Actions/Dashboard/Models/ModelShowAction.ts +82 -174
  98. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +6 -4
  99. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -7
  100. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +47 -17
  101. package/app/Actions/Dashboard/Models/model-write.ts +20 -5
  102. package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +46 -36
  103. package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +14 -6
  104. package/app/Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction.ts +96 -84
  105. package/app/Actions/Dashboard/Notifications/NotificationDeliveryRetryAction.ts +34 -21
  106. package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +2 -4
  107. package/app/Actions/Dashboard/Queries/QueryShowAction.ts +2 -3
  108. package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +2 -4
  109. package/app/Actions/Dashboard/Queue/QueueWorkersAction.ts +2 -4
  110. package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +2 -2
  111. package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +2 -2
  112. package/app/Actions/Dashboard/Rbac/PermissionsIndexAction.ts +2 -3
  113. package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +2 -2
  114. package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +2 -2
  115. package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +2 -6
  116. package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +2 -2
  117. package/app/Actions/Dashboard/Rbac/RolesIndexAction.ts +4 -6
  118. package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +2 -2
  119. package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +2 -9
  120. package/app/Actions/Dashboard/Rbac/UsersListAction.ts +2 -3
  121. package/app/Actions/Dashboard/Rbac/rbac-response.ts +10 -0
  122. package/app/Actions/Dashboard/Realtime/RealtimeStatsAction.ts +2 -4
  123. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +68 -55
  124. package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +34 -16
  125. package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +52 -29
  126. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +102 -71
  127. package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +36 -17
  128. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +39 -26
  129. package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +71 -58
  130. package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +29 -7
  131. package/app/Actions/Dashboard/Teams/team-member-count.ts +4 -2
  132. package/app/Actions/Dashboard/Teams/team-records.test.ts +2 -0
  133. package/app/Actions/Dashboard/Teams/team-records.ts +2 -0
  134. package/app/Actions/Dashboard/dashboard-response.ts +16 -0
  135. package/app/Actions/SendWelcomeEmail.ts +1 -1
  136. package/app/Actions/Teams/AcceptInvitationAction.ts +74 -51
  137. package/app/Actions/Teams/ShowInvitationAction.ts +21 -14
  138. package/app/Actions/Teams/team-response.ts +23 -0
  139. package/app/Mail/OrderConfirmation.ts +1 -1
  140. package/app/Mail/SubscriptionConfirmation.ts +1 -1
  141. package/app/Mail/TeamInvitation.ts +1 -1
  142. package/app/Models/EmailIdempotency.ts +64 -0
  143. package/app/Models/EmailSuppression.ts +52 -0
  144. package/app/Models/EmailWebhookEvent.ts +51 -0
  145. package/app/Models/TeamInvitation.ts +13 -0
  146. package/functions/data.ts +0 -1
  147. package/functions/jobs.ts +0 -1
  148. package/ide/vscode/package.json +1 -1
  149. package/package.json +1 -1
  150. package/resources/components/Dashboard/Data/DataRecords.stx +0 -4
  151. package/resources/components/Dashboard/Jobs/JobDetail.stx +3 -2
  152. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +7 -13
  153. package/resources/components/Dashboard/Models/ModelsOverview.stx +1 -1
  154. package/views/dashboard/.discovered-models.json +30 -0
  155. 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
- const authenticated = await resolveAuthenticatedUser(request as any)
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 user = await db
33
- .selectFrom('users')
34
- .where('id', '=', authenticated.id)
35
- .select(['id', 'email'])
36
- .executeTakeFirst()
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
- const accepted = await db.transaction(async (rawTrx) => {
62
- const trx = rawTrx as unknown as typeof db
63
- const claim = await (trx as any)
64
- .updateTable('team_invitations')
65
- .set({
66
- status: 'accepted',
67
- token_hash: hashInvitationToken(generateInvitationToken()),
68
- accepted_by_user_id: userId,
69
- accepted_at: now,
70
- updated_at: now,
71
- })
72
- .where('id', '=', Number(invitation.id))
73
- .where('token_hash', '=', hashInvitationToken(token))
74
- .where('status', '=', 'pending')
75
- .executeTakeFirst()
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
- if (changedRows(claim) !== 1)
78
- return null
96
+ if (changedRows(claim) !== 1)
97
+ return null
79
98
 
80
- const existing = await (trx as any)
81
- .selectFrom('team_members')
82
- .where('team_id', '=', teamId)
83
- .where('user_id', '=', userId)
84
- .select(['id'])
85
- .executeTakeFirst()
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
- if (!existing) {
88
- await (trx as any)
89
- .insertInto('team_members')
90
- .values({
91
- team_id: teamId,
92
- user_id: userId,
93
- role: String(invitation.role),
94
- status: 'active',
95
- created_at: now,
96
- uuid: randomUUID(),
97
- })
98
- .execute()
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
- await syncTeamMemberCount(teamId, trx)
102
- return { alreadyMember: Boolean(existing) }
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
- const invitation = await (db as any)
19
- .selectFrom('team_invitations')
20
- .innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
21
- .where('team_invitations.token_hash', '=', hashInvitationToken(token))
22
- .select([
23
- 'team_invitations.id as id',
24
- 'team_invitations.team_id as team_id',
25
- 'team_invitations.email as email',
26
- 'team_invitations.role as role',
27
- 'team_invitations.status as status',
28
- 'team_invitations.expires_at as expires_at',
29
- 'teams.name as team_name',
30
- ])
31
- .executeTakeFirst()
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.send({
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`,
@@ -26,7 +26,7 @@ export async function sendSubscriptionConfirmation(options: SubscriptionConfirma
26
26
  subject: `Welcome to ${appName}!`,
27
27
  })
28
28
 
29
- await mail.send({
29
+ await mail.sendOrFail({
30
30
  to: [to],
31
31
  from: {
32
32
  name: config.email.from?.name || appName,
@@ -23,7 +23,7 @@ export async function sendTeamInvitation(options: TeamInvitationMailOptions): Pr
23
23
  subject,
24
24
  })
25
25
 
26
- await mail.send({
26
+ await mail.sendOrFail({
27
27
  to: [options.to],
28
28
  from: {
29
29
  name: config.email.from?.name || appName,
@@ -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
@@ -45,7 +45,6 @@ export interface DashboardSubscriber {
45
45
 
46
46
  export interface DashboardDataResponse<T> {
47
47
  stats: DataStats
48
- error?: string
49
48
  activities?: T[]
50
49
  users?: T[]
51
50
  teams?: T[]
package/functions/jobs.ts CHANGED
@@ -52,7 +52,6 @@ export interface JobListFilters {
52
52
 
53
53
  export interface JobShowResponse {
54
54
  job: DashboardJob | null
55
- error?: string
56
55
  }
57
56
 
58
57
  export async function fetchJobs(filters: JobListFilters = {}): Promise<JobListResponse> {
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.355",
5
+ "version": "0.70.356",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
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.355",
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
- loadError.set(error instanceof Error ? error.message : 'Job could not be loaded.')
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() + ' · ' + sourceLabel()"></p>
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 show the query error instead of reporting zero records.</span>
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",