@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
@@ -1,15 +1,14 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { getAllRoles } from '@stacksjs/auth'
3
- import { response } from '@stacksjs/router'
3
+ import { rbacActionError } from './rbac-response'
4
4
 
5
5
  /**
6
6
  * `GET /api/dashboard/rbac/roles` (stacksjs/stacks#1845).
7
7
  *
8
8
  * Returns every role in the system. The dashboard's permissions page
9
9
  * renders this as a list users can pick from when assigning roles.
10
- * Soft-errors to an empty list rather than 500 so a broken RBAC store
11
- * (e.g., migrations not run) renders an empty-state instead of
12
- * yelling at the user.
10
+ * Operational store failures use a real 500 response so the client can
11
+ * distinguish an unavailable RBAC store from a valid empty role list.
13
12
  */
14
13
  export default new Action({
15
14
  name: 'Dashboard RBAC Roles Index',
@@ -30,8 +29,7 @@ export default new Action({
30
29
  }
31
30
  }
32
31
  catch (err) {
33
- console.error('[dashboard/rbac] RolesIndexAction failed:', err)
34
- return response.json({ roles: [], error: err instanceof Error ? err.message : 'unknown error' }, 500)
32
+ return rbacActionError(err, 'Roles could not be loaded.', 'RolesIndexAction')
35
33
  }
36
34
  },
37
35
  })
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { getUserRoles } from '@stacksjs/auth'
4
4
  import { User } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
+ import { rbacActionError } from './rbac-response'
6
7
 
7
8
  /**
8
9
  * `GET /api/dashboard/rbac/users/:id/roles` (stacksjs/stacks#1845).
@@ -43,8 +44,7 @@ export default new Action({
43
44
  }
44
45
  }
45
46
  catch (err) {
46
- console.error('[dashboard/rbac] UserRolesShowAction failed:', err)
47
- return response.json({ userId, roles: [], error: err instanceof Error ? err.message : 'unknown error' }, 500)
47
+ return rbacActionError(err, 'User roles could not be loaded.', 'UserRolesShowAction')
48
48
  }
49
49
  },
50
50
  })
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { getUserRoles, syncRoles } from '@stacksjs/auth'
4
4
  import { User } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
+ import { rbacActionError } from './rbac-response'
6
7
 
7
8
  interface SyncInput {
8
9
  roles?: unknown
@@ -60,15 +61,7 @@ export default new Action({
60
61
  }
61
62
  }
62
63
  catch (err) {
63
- const msg = err instanceof Error ? err.message : 'unknown error'
64
- // The facade throws "Role 'X' not found" — surface as 400 so
65
- // the UI can show "this role doesn't exist anymore" rather
66
- // than a 500.
67
- if (msg.includes('not found')) {
68
- return response.json({ error: msg }, 400)
69
- }
70
- console.error('[dashboard/rbac] UserRolesSyncAction failed:', err)
71
- return response.json({ error: msg }, 500)
64
+ return rbacActionError(err, 'User roles could not be updated.', 'UserRolesSyncAction')
72
65
  }
73
66
  },
74
67
  })
@@ -1,7 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { User } from '@stacksjs/orm'
3
- import { response } from '@stacksjs/router'
4
3
  import { safeGet } from '../../../../resources/functions/dashboard/data'
4
+ import { rbacActionError } from './rbac-response'
5
5
 
6
6
  /**
7
7
  * `GET /api/dashboard/rbac/users` (stacksjs/stacks#1845).
@@ -40,8 +40,7 @@ export default new Action({
40
40
  }
41
41
  }
42
42
  catch (err) {
43
- console.error('[dashboard/rbac] UsersListAction failed:', err)
44
- return response.json({ users: [], error: err instanceof Error ? err.message : 'unknown error' }, 500)
43
+ return rbacActionError(err, 'Users could not be loaded.', 'UsersListAction')
45
44
  }
46
45
  },
47
46
  })
@@ -0,0 +1,10 @@
1
+ import { RbacEntityNotFoundError } from '@stacksjs/auth'
2
+ import { response } from '@stacksjs/router'
3
+
4
+ export function rbacActionError(error: unknown, fallback: string, action: string): Response {
5
+ if (error instanceof RbacEntityNotFoundError)
6
+ return response.json({ error: error.message }, 400)
7
+
8
+ console.error(`[dashboard/rbac] ${action} failed:`, error)
9
+ return response.json({ error: fallback }, 500)
10
+ }
@@ -1,7 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { config } from '@stacksjs/config'
3
3
  import { Websocket } from '@stacksjs/orm'
4
- import { response } from '@stacksjs/router'
4
+ import { dashboardOperationalError } from '../dashboard-response'
5
5
  import { buildRealtimeStats } from './realtime-stats'
6
6
 
7
7
  export default new Action({
@@ -36,9 +36,7 @@ export default new Action({
36
36
  }
37
37
  }
38
38
  catch (error) {
39
- return response.json({
40
- message: error instanceof Error ? error.message : 'Realtime events could not be loaded.',
41
- }, 503)
39
+ return dashboardOperationalError(error, 'Realtime events could not be loaded.', 'RealtimeStatsAction')
42
40
  }
43
41
  },
44
42
  })
@@ -4,6 +4,7 @@ import { env } from '@stacksjs/env'
4
4
  import { readdirSync } from 'node:fs'
5
5
  import { basename, extname, join } from 'node:path'
6
6
  import process from 'node:process'
7
+ import { dashboardOperationalError } from '../dashboard-response'
7
8
 
8
9
  interface SearchableModel {
9
10
  name: string
@@ -153,8 +154,15 @@ async function loadSearchableModels(): Promise<SearchableModel[]> {
153
154
  }
154
155
 
155
156
  function catalog(): Promise<SearchableModel[]> {
156
- modelCatalog.promise ||= loadSearchableModels()
157
- return modelCatalog.promise
157
+ if (modelCatalog.promise)
158
+ return modelCatalog.promise
159
+
160
+ const loading = loadSearchableModels().catch((error) => {
161
+ modelCatalog.promise = null
162
+ throw error
163
+ })
164
+ modelCatalog.promise = loading
165
+ return loading
158
166
  }
159
167
 
160
168
  function rowValue(row: Record<string, unknown>, field: string): string {
@@ -185,67 +193,72 @@ export default new Action({
185
193
  method: 'GET',
186
194
  apiResponse: true,
187
195
  async handle(request: RequestInstance) {
188
- const q = String(request?.get('q') || '').trim().slice(0, MAX_QUERY_LENGTH)
196
+ const q = String(request.get('q') || '').trim().slice(0, MAX_QUERY_LENGTH)
189
197
  if (!q) return { results: {} }
190
198
 
191
- const { Database } = await import('bun:sqlite')
192
- const db = new Database(env.DB_DATABASE_PATH || 'database/stacks.sqlite', { readonly: true })
193
- const results: Record<string, SearchResult[]> = {}
194
- const unavailable: SearchUnavailable[] = []
195
-
196
199
  try {
197
- for (const model of await catalog()) {
198
- if (Object.keys(results).length >= MAX_GROUPS) break
199
-
200
- const columns = db.query(`PRAGMA table_info(${model.table})`).all() as Array<{ name: string }>
201
- if (columns.length === 0) {
202
- unavailable.push({
203
- model: model.name,
204
- reason: `Table ${model.table} has not been migrated.`,
205
- })
206
- continue
207
- }
200
+ const { Database } = await import('bun:sqlite')
201
+ const db = new Database(env.DB_DATABASE_PATH || 'database/stacks.sqlite', { readonly: true })
202
+ const results: Record<string, SearchResult[]> = {}
203
+ const unavailable: SearchUnavailable[] = []
204
+
205
+ try {
206
+ for (const model of await catalog()) {
207
+ if (Object.keys(results).length >= MAX_GROUPS) break
208
+
209
+ const columns = db.query(`PRAGMA table_info(${model.table})`).all() as Array<{ name: string }>
210
+ if (columns.length === 0) {
211
+ unavailable.push({
212
+ model: model.name,
213
+ reason: `Table ${model.table} has not been migrated.`,
214
+ })
215
+ continue
216
+ }
208
217
 
209
- const available = new Set(columns.map(column => column.name))
210
- const fields = model.fields.filter(field => available.has(field))
211
- if (fields.length === 0) {
212
- unavailable.push({
213
- model: model.name,
214
- reason: 'Declared useSearch fields are not present in the migrated table.',
215
- })
216
- continue
217
- }
218
+ const available = new Set(columns.map(column => column.name))
219
+ const fields = model.fields.filter(field => available.has(field))
220
+ if (fields.length === 0) {
221
+ unavailable.push({
222
+ model: model.name,
223
+ reason: 'Declared useSearch fields are not present in the migrated table.',
224
+ })
225
+ continue
226
+ }
218
227
 
219
- const selected = [...new Set(['id', ...fields])].filter(field => available.has(field))
220
- const where = fields.map(field => `${field} LIKE ? COLLATE NOCASE`).join(' OR ')
221
- const bindings = fields.map(() => `%${q}%`)
222
- let rows: Array<Record<string, unknown>>
223
- try {
224
- rows = db
225
- .query(`SELECT ${selected.join(', ')} FROM ${model.table} WHERE ${where} LIMIT ?`)
226
- .all(...bindings, RESULTS_PER_MODEL) as Array<Record<string, unknown>>
227
- }
228
- catch (error) {
229
- throw new Error(`Could not search model ${model.name}: ${error instanceof Error ? error.message : String(error)}`)
230
- }
231
- if (rows.length === 0) continue
232
-
233
- results[model.name] = rows.map((row, index) => {
234
- const title = resultTitle(model, row)
235
- return {
236
- id: (row.id as string | number | undefined) ?? index,
237
- title,
238
- subtitle: resultSubtitle(model, row, title),
239
- href: `/models/${model.slug}?q=${encodeURIComponent(q)}`,
240
- icon: model.icon,
228
+ const selected = [...new Set(['id', ...fields])].filter(field => available.has(field))
229
+ const where = fields.map(field => `${field} LIKE ? COLLATE NOCASE`).join(' OR ')
230
+ const bindings = fields.map(() => `%${q}%`)
231
+ let rows: Array<Record<string, unknown>>
232
+ try {
233
+ rows = db
234
+ .query(`SELECT ${selected.join(', ')} FROM ${model.table} WHERE ${where} LIMIT ?`)
235
+ .all(...bindings, RESULTS_PER_MODEL) as Array<Record<string, unknown>>
236
+ }
237
+ catch (error) {
238
+ throw new Error(`Could not search model ${model.name}: ${error instanceof Error ? error.message : String(error)}`)
241
239
  }
242
- })
240
+ if (rows.length === 0) continue
241
+
242
+ results[model.name] = rows.map((row, index) => {
243
+ const title = resultTitle(model, row)
244
+ return {
245
+ id: (row.id as string | number | undefined) ?? index,
246
+ title,
247
+ subtitle: resultSubtitle(model, row, title),
248
+ href: `/models/${model.slug}?q=${encodeURIComponent(q)}`,
249
+ icon: model.icon,
250
+ }
251
+ })
252
+ }
243
253
  }
254
+ finally {
255
+ db.close()
256
+ }
257
+
258
+ return { results, unavailable }
244
259
  }
245
- finally {
246
- db.close()
260
+ catch (error) {
261
+ return dashboardOperationalError(error, 'Search results could not be loaded.', 'GlobalSearchAction')
247
262
  }
248
-
249
- return { results, unavailable }
250
263
  },
251
264
  })
@@ -2,7 +2,9 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { response } from '@stacksjs/router'
5
+ import { teamOperationalError } from '../../Teams/team-response'
5
6
  import {
7
+ changedRows,
6
8
  generateInvitationToken,
7
9
  hashInvitationToken,
8
10
  parsePositiveId,
@@ -21,27 +23,43 @@ export default new Action({
21
23
  if (!teamId || !invitationId)
22
24
  return response.json({ message: 'Invalid team or invitation id.' }, 400)
23
25
 
24
- const invitation = await (db as any)
25
- .selectFrom('team_invitations')
26
- .where('id', '=', invitationId)
27
- .where('team_id', '=', teamId)
28
- .select(['id', 'status'])
29
- .executeTakeFirst()
26
+ let invitation
27
+ try {
28
+ invitation = await (db as any)
29
+ .selectFrom('team_invitations')
30
+ .where('id', '=', invitationId)
31
+ .where('team_id', '=', teamId)
32
+ .select(['id', 'status', 'token_hash'])
33
+ .executeTakeFirst()
34
+ }
35
+ catch (error) {
36
+ return teamOperationalError(error, 'Invitation could not be loaded.', 'TeamInvitationDestroyAction.lookup', 500)
37
+ }
30
38
  if (!invitation)
31
39
  return response.json({ message: 'Invitation not found.' }, 404)
32
40
  if (invitation.status !== 'pending')
33
41
  return response.json({ message: 'Only pending invitations can be revoked.' }, 409)
34
42
 
35
- await (db as any)
36
- .updateTable('team_invitations')
37
- .set({
38
- status: 'revoked',
39
- token_hash: hashInvitationToken(generateInvitationToken()),
40
- updated_at: sqlTimestamp(),
41
- })
42
- .where('id', '=', invitationId)
43
- .where('team_id', '=', teamId)
44
- .execute()
43
+ try {
44
+ const result = await (db as any)
45
+ .updateTable('team_invitations')
46
+ .set({
47
+ status: 'revoked',
48
+ token_hash: hashInvitationToken(generateInvitationToken()),
49
+ pending_key: null,
50
+ updated_at: sqlTimestamp(),
51
+ })
52
+ .where('id', '=', invitationId)
53
+ .where('team_id', '=', teamId)
54
+ .where('token_hash', '=', String(invitation.token_hash))
55
+ .where('status', '=', 'pending')
56
+ .executeTakeFirst()
57
+ if (changedRows(result) !== 1)
58
+ return response.json({ message: 'The invitation changed before it could be revoked.' }, 409)
59
+ }
60
+ catch (error) {
61
+ return teamOperationalError(error, 'Invitation could not be revoked.', 'TeamInvitationDestroyAction.update', 500)
62
+ }
45
63
 
46
64
  return response.noContent()
47
65
  },
@@ -1,9 +1,12 @@
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 } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
6
+ import { teamOperationalError } from '../../Teams/team-response'
5
7
  import { deliverTeamInvitation } from './team-invitation-delivery'
6
8
  import {
9
+ changedRows,
7
10
  generateInvitationToken,
8
11
  hashInvitationToken,
9
12
  invitationExpiresAt,
@@ -23,40 +26,59 @@ export default new Action({
23
26
  if (!teamId || !invitationId)
24
27
  return response.json({ message: 'Invalid team or invitation id.' }, 400)
25
28
 
26
- const invitation = await (db as any)
27
- .selectFrom('team_invitations')
28
- .innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
29
- .where('team_invitations.id', '=', invitationId)
30
- .where('team_invitations.team_id', '=', teamId)
31
- .select([
32
- 'team_invitations.id as id',
33
- 'team_invitations.email as email',
34
- 'team_invitations.role as role',
35
- 'team_invitations.status as status',
36
- 'teams.name as team_name',
37
- ])
38
- .executeTakeFirst()
29
+ let invitation
30
+ try {
31
+ invitation = await (db as any)
32
+ .selectFrom('team_invitations')
33
+ .innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
34
+ .where('team_invitations.id', '=', invitationId)
35
+ .where('team_invitations.team_id', '=', teamId)
36
+ .select([
37
+ 'team_invitations.id as id',
38
+ 'team_invitations.email as email',
39
+ 'team_invitations.role as role',
40
+ 'team_invitations.status as status',
41
+ 'team_invitations.token_hash as token_hash',
42
+ 'teams.name as team_name',
43
+ ])
44
+ .executeTakeFirst()
45
+ }
46
+ catch (error) {
47
+ return teamOperationalError(error, 'Invitation could not be loaded.', 'TeamInvitationResendAction.lookup', 500)
48
+ }
39
49
  if (!invitation)
40
50
  return response.json({ message: 'Invitation not found.' }, 404)
41
51
  if (!['pending', 'expired'].includes(String(invitation.status)))
42
52
  return response.json({ message: 'Only pending or expired invitations can be resent.' }, 409)
43
53
 
44
54
  const token = generateInvitationToken()
55
+ const tokenHash = hashInvitationToken(token)
45
56
  const expiresAt = invitationExpiresAt()
46
- await (db as any)
47
- .updateTable('team_invitations')
48
- .set({
49
- token_hash: hashInvitationToken(token),
50
- status: 'pending',
51
- delivery_status: 'pending',
52
- delivery_error: null,
53
- expires_at: expiresAt,
54
- delivered_at: null,
55
- updated_at: sqlTimestamp(),
56
- })
57
- .where('id', '=', invitationId)
58
- .where('team_id', '=', teamId)
59
- .execute()
57
+ try {
58
+ const result = await (db as any)
59
+ .updateTable('team_invitations')
60
+ .set({
61
+ token_hash: tokenHash,
62
+ pending_key: `${teamId}:${String(invitation.email).trim().toLowerCase()}`,
63
+ status: 'pending',
64
+ delivery_status: 'pending',
65
+ delivery_error: null,
66
+ expires_at: expiresAt,
67
+ delivered_at: null,
68
+ updated_at: sqlTimestamp(),
69
+ })
70
+ .where('id', '=', invitationId)
71
+ .where('team_id', '=', teamId)
72
+ .where('token_hash', '=', String(invitation.token_hash))
73
+ .executeTakeFirst()
74
+ if (changedRows(result) !== 1)
75
+ return response.json({ message: 'The invitation changed before it could be resent.' }, 409)
76
+ }
77
+ catch (error) {
78
+ if (isUniqueViolation(error))
79
+ return response.json({ message: 'Another pending invitation already exists for this email.' }, 409)
80
+ return teamOperationalError(error, 'Invitation could not be refreshed.', 'TeamInvitationResendAction.update', 500)
81
+ }
60
82
 
61
83
  try {
62
84
  await deliverTeamInvitation({
@@ -65,10 +87,11 @@ export default new Action({
65
87
  teamName: String(invitation.team_name),
66
88
  role: String(invitation.role),
67
89
  token,
90
+ tokenHash,
68
91
  })
69
92
  }
70
- catch {
71
- return response.json({ message: 'The invitation was refreshed, but email delivery failed.' }, 502)
93
+ catch (error) {
94
+ return teamOperationalError(error, 'The invitation was refreshed, but email delivery failed.', 'TeamInvitationResendAction.delivery', 502)
72
95
  }
73
96
 
74
97
  return {