@stacksjs/defaults 0.75.36 → 0.75.38

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.
@@ -20,7 +20,7 @@
20
20
 
21
21
  import type { DashboardData, FailedTransition, PreviousRunState } from '@stacksjs/github'
22
22
  import { dashboard as dashboardConfig } from '@stacksjs/config'
23
- import { db } from '@stacksjs/database/runtime'
23
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
24
24
  import { detectNewlyFailedRuns } from '@stacksjs/github'
25
25
  import { notify } from '@stacksjs/notifications'
26
26
 
@@ -60,13 +60,17 @@ async function upsertSnapshotStates(snapshot: DashboardData): Promise<void> {
60
60
  const nowIso = new Date().toISOString()
61
61
  for (const r of snapshot.repos) {
62
62
  const runId = parseRunIdFromUrl(r.runUrl)
63
- // Two-step upsert — works across SQLite/MySQL/Postgres without
64
- // dialect-specific ON CONFLICT shapes. Each repo is its own
65
- // round-trip, but the table is small (one row per repo, so
66
- // typically O(50) for a busy fleet) and this runs in the
67
- // background after the response goes out.
63
+ // Two-step upsert, which avoids the dialect-specific ON CONFLICT
64
+ // shapes. Each repo is its own round-trip, but the table is small
65
+ // (one row per repo, so typically O(50) for a busy fleet) and this
66
+ // runs in the background after the response goes out.
67
+ //
68
+ // The upsert shape is portable; the placeholder is not. Postgres
69
+ // numbers its parameters, so the `?` this used to carry bound
70
+ // nothing there (stacksjs/stacks#2846).
71
+ const { param } = sqlHelpers(getDatabaseDialect())
68
72
  const existing = await db.unsafe(
69
- 'SELECT repo_full_name FROM ci_run_states WHERE repo_full_name = ? LIMIT 1',
73
+ `SELECT repo_full_name FROM ci_run_states WHERE repo_full_name = ${param(1)} LIMIT 1`,
70
74
  [r.fullName],
71
75
  ).execute() as Array<{ repo_full_name: string }>
72
76
  if (existing?.length) {
@@ -26,7 +26,7 @@
26
26
 
27
27
  import type { DashboardData, PressureAction, RunnerAlertState, RunnerSample } from '@stacksjs/github'
28
28
  import { dashboard as dashboardConfig } from '@stacksjs/config'
29
- import { db } from '@stacksjs/database/runtime'
29
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
30
30
  import { detectRunnerPressure } from '@stacksjs/github'
31
31
  import { notify } from '@stacksjs/notifications'
32
32
 
@@ -69,8 +69,9 @@ async function pruneOldSamples(retentionHours: number, nowMs: number): Promise<v
69
69
 
70
70
  async function loadWindowSamples(windowMinutes: number, nowMs: number): Promise<RunnerSample[]> {
71
71
  const cutoffIso = new Date(nowMs - windowMinutes * 60_000).toISOString()
72
+ const { param } = sqlHelpers(getDatabaseDialect())
72
73
  const rows = await db.unsafe(
73
- 'SELECT org, running, queued, cap, sampled_at FROM ci_runner_samples WHERE sampled_at >= ? ORDER BY sampled_at ASC',
74
+ `SELECT org, running, queued, cap, sampled_at FROM ci_runner_samples WHERE sampled_at >= ${param(1)} ORDER BY sampled_at ASC`,
74
75
  [cutoffIso],
75
76
  ).execute() as unknown as SampleRow[]
76
77
 
@@ -102,11 +103,15 @@ async function loadAlertStates(): Promise<Map<string, RunnerAlertState>> {
102
103
 
103
104
  async function upsertAlertState(org: string, alerting: boolean, nowIso: string): Promise<void> {
104
105
  // Cross-dialect upsert: SELECT-then-INSERT/UPDATE. Same shape as
105
- // the failure-notifier's state writes — keep the patterns
106
- // consistent so future maintainers don't second-guess the
106
+ // the failure-notifier's state writes, so keep the patterns
107
+ // consistent and future maintainers don't second-guess the
107
108
  // dialect choice in one place vs another.
109
+ //
110
+ // That portability covers the upsert, not the placeholder, which has
111
+ // to be rendered per dialect (stacksjs/stacks#2846).
112
+ const { param } = sqlHelpers(getDatabaseDialect())
108
113
  const existing = await db.unsafe(
109
- 'SELECT org FROM ci_runner_alert_states WHERE org = ? LIMIT 1',
114
+ `SELECT org FROM ci_runner_alert_states WHERE org = ${param(1)} LIMIT 1`,
110
115
  [org],
111
116
  ).execute() as Array<{ org: string }>
112
117
 
@@ -241,12 +246,13 @@ export async function fetchRunnerHistory(
241
246
 
242
247
  // ASC-by-newest then re-reverse so we get the N most recent samples
243
248
  // in ASC order (oldest → newest) — what the sparkline path needs.
249
+ const { param } = sqlHelpers(getDatabaseDialect())
244
250
  const rows = await db.unsafe(
245
251
  `SELECT org, running, queued, cap, sampled_at
246
252
  FROM ci_runner_samples
247
- WHERE org = ? AND sampled_at >= ?
253
+ WHERE org = ${param(1)} AND sampled_at >= ${param(2)}
248
254
  ORDER BY sampled_at DESC
249
- LIMIT ?`,
255
+ LIMIT ${param(3)}`,
250
256
  [org, cutoffIso, limit],
251
257
  ).execute() as unknown as SampleRow[]
252
258
 
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { kanbanActionError, kanbanError } from './kanban-response'
5
5
 
6
6
  /**
@@ -33,19 +33,22 @@ export default new Action({
33
33
  try {
34
34
  await db.transaction(async (rawTrx) => {
35
35
  const qb = rawTrx as unknown as typeof db
36
+ // Postgres numbers its parameters, so the `?` these carried bound
37
+ // nothing there (stacksjs/stacks#2846).
38
+ const { param } = sqlHelpers(getDatabaseDialect())
36
39
  // Pivots + card-scoped children first — they reference cards.
37
40
  await qb.unsafe(
38
- 'DELETE FROM card_labels WHERE card_id IN (SELECT id FROM cards WHERE board_id = ?)',
41
+ `DELETE FROM card_labels WHERE card_id IN (SELECT id FROM cards WHERE board_id = ${param(1)})`,
39
42
  [id],
40
43
  ).execute()
41
44
  await qb.unsafe(
42
- 'DELETE FROM card_assignees WHERE card_id IN (SELECT id FROM cards WHERE board_id = ?)',
45
+ `DELETE FROM card_assignees WHERE card_id IN (SELECT id FROM cards WHERE board_id = ${param(1)})`,
43
46
  [id],
44
47
  ).execute()
45
48
  // Card comments use the same
46
49
  // card-scoped pattern as the pivots.
47
50
  await qb.unsafe(
48
- 'DELETE FROM card_comments WHERE card_id IN (SELECT id FROM cards WHERE board_id = ?)',
51
+ `DELETE FROM card_comments WHERE card_id IN (SELECT id FROM cards WHERE board_id = ${param(1)})`,
49
52
  [id],
50
53
  ).execute()
51
54
  // Cards (denormalised board_id avoids the column join).
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { modelBoolean } from './kanban-model'
5
5
  import { kanbanActionError, kanbanError } from './kanban-response'
6
6
 
@@ -83,8 +83,11 @@ export default new Action({
83
83
  }
84
84
 
85
85
  try {
86
+ // Postgres numbers its parameters, so the `?` these carried bound
87
+ // nothing there (stacksjs/stacks#2846).
88
+ const { param } = sqlHelpers(getDatabaseDialect())
86
89
  const boards = await db.unsafe(
87
- 'SELECT * FROM boards WHERE id = ? LIMIT 1',
90
+ `SELECT * FROM boards WHERE id = ${param(1)} LIMIT 1`,
88
91
  [id],
89
92
  ).execute() as unknown as BoardRow[]
90
93
  const board = boards[0]
@@ -94,15 +97,15 @@ export default new Action({
94
97
 
95
98
  const [columns, cards, labels] = await Promise.all([
96
99
  db.unsafe(
97
- 'SELECT * FROM board_columns WHERE board_id = ? ORDER BY position ASC, id ASC',
100
+ `SELECT * FROM board_columns WHERE board_id = ${param(1)} ORDER BY position ASC, id ASC`,
98
101
  [id],
99
102
  ).execute() as unknown as Promise<ColumnRow[]>,
100
103
  db.unsafe(
101
- 'SELECT * FROM cards WHERE board_id = ? AND archived = false ORDER BY column_id ASC, position ASC, id ASC',
104
+ `SELECT * FROM cards WHERE board_id = ${param(1)} AND archived = false ORDER BY column_id ASC, position ASC, id ASC`,
102
105
  [id],
103
106
  ).execute() as unknown as Promise<CardRow[]>,
104
107
  db.unsafe(
105
- 'SELECT id, board_id, name, color FROM labels WHERE board_id = ? ORDER BY name ASC',
108
+ `SELECT id, board_id, name, color FROM labels WHERE board_id = ${param(1)} ORDER BY name ASC`,
106
109
  [id],
107
110
  ).execute() as unknown as Promise<LabelRow[]>,
108
111
  ])
@@ -116,7 +119,7 @@ export default new Action({
116
119
  `SELECT cl.card_id, l.id, l.name, l.color
117
120
  FROM card_labels cl
118
121
  JOIN labels l ON l.id = cl.label_id
119
- WHERE cl.card_id IN (SELECT id FROM cards WHERE board_id = ? AND archived = false)
122
+ WHERE cl.card_id IN (SELECT id FROM cards WHERE board_id = ${param(1)} AND archived = false)
120
123
  ORDER BY l.name ASC`,
121
124
  [id],
122
125
  ).execute() as Promise<Array<{ card_id: number, id: number, name: string, color: string }>>,
@@ -124,7 +127,7 @@ export default new Action({
124
127
  `SELECT ca.card_id, ca.user_id, u.name, u.email
125
128
  FROM card_assignees ca
126
129
  LEFT JOIN users u ON u.id = ca.user_id
127
- WHERE ca.card_id IN (SELECT id FROM cards WHERE board_id = ? AND archived = false)`,
130
+ WHERE ca.card_id IN (SELECT id FROM cards WHERE board_id = ${param(1)} AND archived = false)`,
128
131
  [id],
129
132
  ).execute() as Promise<Array<{ card_id: number, user_id: number, name: string | null, email: string | null }>>,
130
133
  ])
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { kanbanActionError, kanbanError } from './kanban-response'
5
5
 
6
6
  interface SyncInput {
@@ -48,13 +48,16 @@ export default new Action({
48
48
  const uniqueUserIds = Array.from(new Set(userIds))
49
49
 
50
50
  try {
51
- const cardRows = await db.unsafe('SELECT id FROM cards WHERE id = ? LIMIT 1', [cardId]).execute() as Array<{ id: number }>
51
+ // Postgres numbers its parameters, so the `?` these carried bound
52
+ // nothing there, the dynamic IN lists included (stacksjs/stacks#2846).
53
+ const { param } = sqlHelpers(getDatabaseDialect())
54
+ const cardRows = await db.unsafe(`SELECT id FROM cards WHERE id = ${param(1)} LIMIT 1`, [cardId]).execute() as Array<{ id: number }>
52
55
  if (!cardRows?.length)
53
56
  return kanbanError('Card not found.', 404)
54
57
 
55
58
  // Validate user ids exist.
56
59
  if (uniqueUserIds.length > 0) {
57
- const placeholders = uniqueUserIds.map(() => '?').join(',')
60
+ const placeholders = uniqueUserIds.map((_, index) => param(index + 1)).join(',')
58
61
  const userRows = await db.unsafe(
59
62
  `SELECT id FROM users WHERE id IN (${placeholders})`,
60
63
  uniqueUserIds,
@@ -84,7 +87,7 @@ export default new Action({
84
87
  // Return the resolved user rows for the optimistic UI to confirm.
85
88
  let assignees: Array<{ userId: number, name: string | null, email: string | null }> = []
86
89
  if (uniqueUserIds.length > 0) {
87
- const placeholders = uniqueUserIds.map(() => '?').join(',')
90
+ const placeholders = uniqueUserIds.map((_, index) => param(index + 1)).join(',')
88
91
  const rows = await db.unsafe(
89
92
  `SELECT id, name, email FROM users WHERE id IN (${placeholders}) ORDER BY name ASC`,
90
93
  uniqueUserIds,
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { kanbanActionError, kanbanError } from './kanban-response'
5
5
 
6
6
  interface SyncInput {
@@ -47,8 +47,11 @@ export default new Action({
47
47
 
48
48
  try {
49
49
  // Resolve the card → board so we can validate label scope.
50
+ // Postgres numbers its parameters, so the `?` these carried bound
51
+ // nothing there (stacksjs/stacks#2846).
52
+ const { param } = sqlHelpers(getDatabaseDialect())
50
53
  const cardRows = await db.unsafe(
51
- 'SELECT id, board_id FROM cards WHERE id = ? LIMIT 1',
54
+ `SELECT id, board_id FROM cards WHERE id = ${param(1)} LIMIT 1`,
52
55
  [cardId],
53
56
  ).execute() as Array<{ id: number, board_id: number }>
54
57
  const card = cardRows?.[0]
@@ -58,9 +61,10 @@ export default new Action({
58
61
  // Every label must belong to the same board (labels are
59
62
  // board-scoped through `labels.board_id`).
60
63
  if (uniqueLabelIds.length > 0) {
61
- const placeholders = uniqueLabelIds.map(() => '?').join(',')
64
+ // The IN list is numbered alongside the trailing board id.
65
+ const placeholders = uniqueLabelIds.map((_, index) => param(index + 1)).join(',')
62
66
  const labelRows = await db.unsafe(
63
- `SELECT id FROM labels WHERE id IN (${placeholders}) AND board_id = ?`,
67
+ `SELECT id FROM labels WHERE id IN (${placeholders}) AND board_id = ${param(uniqueLabelIds.length + 1)}`,
64
68
  [...uniqueLabelIds, card.board_id],
65
69
  ).execute() as Array<{ id: number }>
66
70
  if (labelRows.length !== uniqueLabelIds.length)
@@ -83,7 +87,7 @@ export default new Action({
83
87
  // confirm its in-flight state matches what the server stored.
84
88
  let labels: Array<{ id: number, name: string, color: string }> = []
85
89
  if (uniqueLabelIds.length > 0) {
86
- const placeholders = uniqueLabelIds.map(() => '?').join(',')
90
+ const placeholders = uniqueLabelIds.map((_, index) => param(index + 1)).join(',')
87
91
  labels = await db.unsafe(
88
92
  `SELECT id, name, color FROM labels WHERE id IN (${placeholders}) ORDER BY name ASC`,
89
93
  uniqueLabelIds,
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { modelBoolean } from './kanban-model'
5
5
  import { kanbanActionError, kanbanError } from './kanban-response'
6
6
 
@@ -47,8 +47,11 @@ export default new Action({
47
47
  }
48
48
 
49
49
  try {
50
+ // Postgres numbers its parameters, so the `?` these carried bound
51
+ // nothing there (stacksjs/stacks#2846).
52
+ const { param } = sqlHelpers(getDatabaseDialect())
50
53
  const cardRows = await db.unsafe(
51
- 'SELECT * FROM cards WHERE id = ? LIMIT 1',
54
+ `SELECT * FROM cards WHERE id = ${param(1)} LIMIT 1`,
52
55
  [id],
53
56
  ).execute() as unknown as CardRow[]
54
57
  const card = cardRows?.[0]
@@ -61,7 +64,7 @@ export default new Action({
61
64
  `SELECT l.id, l.name, l.color
62
65
  FROM card_labels cl
63
66
  JOIN labels l ON l.id = cl.label_id
64
- WHERE cl.card_id = ?
67
+ WHERE cl.card_id = ${param(1)}
65
68
  ORDER BY l.name ASC`,
66
69
  [id],
67
70
  ).execute() as Promise<Array<{ id: number, name: string, color: string }>>,
@@ -69,14 +72,14 @@ export default new Action({
69
72
  `SELECT ca.user_id, ca.assigned_by_user_id, ca.created_at, u.name, u.email
70
73
  FROM card_assignees ca
71
74
  LEFT JOIN users u ON u.id = ca.user_id
72
- WHERE ca.card_id = ?`,
75
+ WHERE ca.card_id = ${param(1)}`,
73
76
  [id],
74
77
  ).execute() as Promise<Array<{ user_id: number, assigned_by_user_id: number | null, created_at: string | null, name: string | null, email: string | null }>>,
75
78
  db.unsafe(
76
79
  `SELECT cc.id, cc.uuid, cc.user_id, cc.body, cc.created_at, cc.updated_at, u.name, u.email
77
80
  FROM card_comments cc
78
81
  LEFT JOIN users u ON u.id = cc.user_id
79
- WHERE cc.card_id = ?
82
+ WHERE cc.card_id = ${param(1)}
80
83
  ORDER BY cc.created_at ASC, cc.id ASC`,
81
84
  [id],
82
85
  ).execute() as Promise<Array<{ id: number, uuid: string | null, user_id: number | null, body: string, created_at: string | null, updated_at: string | null, name: string | null, email: string | null }>>,
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { BoardColumn, Card } from '@stacksjs/orm'
5
5
  import { kanbanActionError, kanbanError } from './kanban-response'
6
6
 
@@ -54,8 +54,9 @@ export default new Action({
54
54
  if (!Number.isFinite(boardId) || boardId <= 0)
55
55
  return kanbanError('Column is not attached to a valid board.', 409)
56
56
 
57
+ const { param } = sqlHelpers(getDatabaseDialect())
57
58
  const maxRow = await db.unsafe(
58
- 'SELECT COALESCE(MAX(position), -1) AS m FROM cards WHERE column_id = ?',
59
+ `SELECT COALESCE(MAX(position), -1) AS m FROM cards WHERE column_id = ${param(1)}`,
59
60
  [columnId],
60
61
  ).execute() as Array<{ m: number }>
61
62
  const nextPosition = (Number(maxRow?.[0]?.m ?? -1) + 1) || 0
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { kanbanActionError, kanbanError } from './kanban-response'
5
5
 
6
6
  interface ColumnReorder {
@@ -86,8 +86,11 @@ export default new Action({
86
86
 
87
87
  try {
88
88
  // Verify all columns belong to the same board.
89
+ // Postgres numbers its parameters, so every IN list below is numbered
90
+ // rather than repeating `?` (stacksjs/stacks#2846).
91
+ const { param } = sqlHelpers(getDatabaseDialect())
89
92
  const columnIds = reorders.map(r => r.columnId)
90
- const colPlaceholders = columnIds.map(() => '?').join(',')
93
+ const colPlaceholders = columnIds.map((_, index) => param(index + 1)).join(',')
91
94
  const colRows = await db.unsafe(
92
95
  `SELECT id, board_id FROM board_columns WHERE id IN (${colPlaceholders})`,
93
96
  columnIds,
@@ -105,9 +108,9 @@ export default new Action({
105
108
  // page from moving cards in from a sibling board).
106
109
  if (allCardIds.size > 0) {
107
110
  const cardIdList = Array.from(allCardIds)
108
- const cardPlaceholders = cardIdList.map(() => '?').join(',')
111
+ const cardPlaceholders = cardIdList.map((_, index) => param(index + 1)).join(',')
109
112
  const cardRows = await db.unsafe(
110
- `SELECT id FROM cards WHERE id IN (${cardPlaceholders}) AND board_id = ?`,
113
+ `SELECT id FROM cards WHERE id IN (${cardPlaceholders}) AND board_id = ${param(cardIdList.length + 1)}`,
111
114
  [...cardIdList, boardId],
112
115
  ).execute() as Array<{ id: number }>
113
116
  if (cardRows.length !== cardIdList.length) {
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { kanbanActionError, kanbanError } from './kanban-response'
5
5
 
6
6
  /**
@@ -29,17 +29,20 @@ export default new Action({
29
29
  try {
30
30
  await db.transaction(async (rawTrx) => {
31
31
  const qb = rawTrx as unknown as typeof db
32
+ // Postgres numbers its parameters, so the `?` these carried bound
33
+ // nothing there (stacksjs/stacks#2846).
34
+ const { param } = sqlHelpers(getDatabaseDialect())
32
35
  await qb.unsafe(
33
- 'DELETE FROM card_labels WHERE card_id IN (SELECT id FROM cards WHERE column_id = ?)',
36
+ `DELETE FROM card_labels WHERE card_id IN (SELECT id FROM cards WHERE column_id = ${param(1)})`,
34
37
  [id],
35
38
  ).execute()
36
39
  await qb.unsafe(
37
- 'DELETE FROM card_assignees WHERE card_id IN (SELECT id FROM cards WHERE column_id = ?)',
40
+ `DELETE FROM card_assignees WHERE card_id IN (SELECT id FROM cards WHERE column_id = ${param(1)})`,
38
41
  [id],
39
42
  ).execute()
40
43
  // Card comments are card-scoped children.
41
44
  await qb.unsafe(
42
- 'DELETE FROM card_comments WHERE card_id IN (SELECT id FROM cards WHERE column_id = ?)',
45
+ `DELETE FROM card_comments WHERE card_id IN (SELECT id FROM cards WHERE column_id = ${param(1)})`,
43
46
  [id],
44
47
  ).execute()
45
48
  await qb.deleteFrom('cards').where('column_id', '=', id).execute()
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { Board, BoardColumn } from '@stacksjs/orm'
5
5
  import { kanbanActionError, kanbanError } from './kanban-response'
6
6
 
@@ -47,8 +47,9 @@ export default new Action({
47
47
  return kanbanError('Board not found.', 404)
48
48
  }
49
49
 
50
+ const { param } = sqlHelpers(getDatabaseDialect())
50
51
  const maxRow = await db.unsafe(
51
- 'SELECT COALESCE(MAX(position), -1) AS m FROM board_columns WHERE board_id = ?',
52
+ `SELECT COALESCE(MAX(position), -1) AS m FROM board_columns WHERE board_id = ${param(1)}`,
52
53
  [boardId],
53
54
  ).execute() as Array<{ m: number }>
54
55
  const nextPosition = (Number(maxRow?.[0]?.m ?? -1) + 1) || 0
@@ -1,6 +1,6 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database/runtime'
3
+ import { db, getDatabaseDialect, sqlHelpers } from '@stacksjs/database/runtime'
4
4
  import { kanbanActionError, kanbanError } from './kanban-response'
5
5
 
6
6
  interface ReorderInput {
@@ -53,9 +53,13 @@ export default new Action({
53
53
  try {
54
54
  // Verify every column actually belongs to the named board.
55
55
  // Single round-trip via a count of rows that match both criteria.
56
- const placeholders = ids.map(() => '?').join(',')
56
+ // Postgres numbers its parameters, so the IN list has to be numbered
57
+ // alongside the trailing board id rather than repeating `?`
58
+ // (stacksjs/stacks#2846).
59
+ const { param } = sqlHelpers(getDatabaseDialect())
60
+ const placeholders = ids.map((_, index) => param(index + 1)).join(',')
57
61
  const matchRows = await db.unsafe(
58
- `SELECT COUNT(*) AS c FROM board_columns WHERE id IN (${placeholders}) AND board_id = ?`,
62
+ `SELECT COUNT(*) AS c FROM board_columns WHERE id IN (${placeholders}) AND board_id = ${param(ids.length + 1)}`,
59
63
  [...ids, boardId],
60
64
  ).execute() as Array<{ c: number }>
61
65
  const matched = Number(matchRows?.[0]?.c ?? 0)
package/app/Listener.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { registerAppListeners } from '@stacksjs/events'
1
+ import { bootAppListeners } from '@stacksjs/events'
2
2
  import { path as p } from '@stacksjs/path'
3
3
 
4
4
  /**
@@ -33,5 +33,8 @@ export async function handleEvents(): Promise<number> {
33
33
  // command, a test runner or a worker started from a subdirectory would
34
34
  // otherwise look for `app/Events.ts` beside wherever it happened to be
35
35
  // launched, find nothing, and register no listeners without saying so.
36
- return registerAppListeners({ base: p.projectPath() })
36
+ //
37
+ // `bootAppListeners` so this and the framework's own boot register once
38
+ // between them in any process, whichever runs first.
39
+ return bootAppListeners({ base: p.projectPath() })
37
40
  }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.36",
5
+ "version": "0.75.38",
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.75.36",
5
+ "version": "0.75.38",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.75.36",
58
+ "@stacksjs/mobile": "^0.75.38",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }