kanbo-cli 0.2.0 → 0.3.0
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/CHANGELOG.md +173 -0
- package/LICENSE +75 -21
- package/README.md +19 -48
- package/dist/{better-sqlite3-fU3G4xnX.cjs → better-sqlite3-DPdSZ6sH.cjs} +244 -247
- package/dist/cli-main.cjs +51208 -0
- package/dist/cli.cjs +32 -45216
- package/dist/dist-IHRTF-b5.cjs +2552 -0
- package/dist/lib/{tool-names-WfH4IURz.d.cts → approval-BsBk2xVr.d.cts} +72 -3
- package/dist/lib/{tool-names-3q_GEZej.d.mts → approval-LRZ6ontC.d.mts} +72 -3
- package/dist/lib/{board-store-BvWn9xTB.d.mts → board-store-D_Mpfb84.d.mts} +75 -2
- package/dist/lib/{board-store-BfszzAJB.d.cts → board-store-Dx-mw2I6.d.cts} +75 -2
- package/dist/lib/index.cjs +8 -2
- package/dist/lib/index.d.cts +202 -91
- package/dist/lib/index.d.mts +202 -91
- package/dist/lib/index.mjs +3 -3
- package/dist/lib/{manifest-RDyoGrVR.cjs → manifest-QPoQl8tn.cjs} +6881 -1556
- package/dist/lib/{manifest-BwpC-4BO.mjs → manifest-v2e9x_C8.mjs} +6795 -1556
- package/dist/lib/mcp/index.cjs +156 -21
- package/dist/lib/mcp/index.d.cts +140 -5
- package/dist/lib/mcp/index.d.mts +140 -5
- package/dist/lib/mcp/index.mjs +152 -22
- package/dist/lib/{migrate-DnMjTCvu.cjs → migrate-Dca5WdYd.cjs} +144 -14
- package/dist/lib/{migrate-BhYi9cXw.mjs → migrate-DtBeW3Fs.mjs} +134 -16
- package/dist/lib/{migrations-path-Ciwan537.mjs → migrations-path-ClaYRNwk.mjs} +5 -7
- package/dist/lib/{migrations-path-CZxVOgeD.cjs → migrations-path-DBCE17dT.cjs} +5 -7
- package/dist/lib/postgres/index.cjs +2 -4
- package/dist/lib/postgres/index.d.cts +2 -3
- package/dist/lib/postgres/index.d.mts +2 -3
- package/dist/lib/postgres/index.mjs +2 -4
- package/dist/lib/{roles.sql-CvyCGt38.mjs → roles.sql-BoOZSkBp.mjs} +96 -5
- package/dist/lib/{roles.sql-B9rt-x4o.cjs → roles.sql-E58x7mLS.cjs} +95 -4
- package/dist/lib/schema-D0MyXcej.d.cts +1 -2
- package/dist/lib/schema-D0MyXcej.d.mts +1 -2
- package/dist/lib/schema-epoch-ZyumVL_A.d.cts +1 -2
- package/dist/lib/schema-epoch-ZyumVL_A.d.mts +1 -2
- package/dist/lib/sqlite/index.cjs +2 -2
- package/dist/lib/sqlite/index.d.cts +3 -4
- package/dist/lib/sqlite/index.d.mts +3 -4
- package/dist/lib/sqlite/index.mjs +2 -2
- package/dist/lib/sqlite/schema.cjs +0 -2
- package/dist/lib/sqlite/schema.mjs +0 -2
- package/package.json +14 -11
- package/dist/better-sqlite3-fU3G4xnX.cjs.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/dist-BsCwQFEl.cjs +0 -989
- package/dist/dist-BsCwQFEl.cjs.map +0 -1
- package/dist/lib/manifest-BwpC-4BO.mjs.map +0 -1
- package/dist/lib/manifest-RDyoGrVR.cjs.map +0 -1
- package/dist/lib/mcp/index.cjs.map +0 -1
- package/dist/lib/mcp/index.mjs.map +0 -1
- package/dist/lib/migrate-BhYi9cXw.mjs.map +0 -1
- package/dist/lib/migrate-DnMjTCvu.cjs.map +0 -1
- package/dist/lib/migrations-path-CZxVOgeD.cjs.map +0 -1
- package/dist/lib/migrations-path-Ciwan537.mjs.map +0 -1
- package/dist/lib/postgres/index.cjs.map +0 -1
- package/dist/lib/postgres/index.mjs.map +0 -1
- package/dist/lib/roles.sql-B9rt-x4o.cjs.map +0 -1
- package/dist/lib/roles.sql-CvyCGt38.mjs.map +0 -1
- package/dist/lib/sqlite/schema.cjs.map +0 -1
- package/dist/lib/sqlite/schema.mjs.map +0 -1
- package/drizzle-postgres/0000_board.sql +0 -135
- package/drizzle-postgres/0001_history_order.sql +0 -2
- package/drizzle-postgres/0002_issue_pull_requests.sql +0 -16
- package/drizzle-postgres/0003_milestone_start_date.sql +0 -1
- package/drizzle-postgres/0004_status_entry_rules.sql +0 -1
- package/drizzle-postgres/0005_run_external_session_ref.sql +0 -1
- package/drizzle-postgres/README.md +0 -61
- package/drizzle-postgres/meta/0000_snapshot.json +0 -1078
- package/drizzle-postgres/meta/0001_snapshot.json +0 -1090
- package/drizzle-postgres/meta/0002_snapshot.json +0 -1239
- package/drizzle-postgres/meta/0003_snapshot.json +0 -1245
- package/drizzle-postgres/meta/0004_snapshot.json +0 -1251
- package/drizzle-postgres/meta/0005_snapshot.json +0 -1257
- package/drizzle-postgres/meta/_journal.json +0 -48
- package/drizzle-sqlite/0000_board.sql +0 -133
- package/drizzle-sqlite/0001_issue_pull_requests.sql +0 -15
- package/drizzle-sqlite/0002_milestone_start_date.sql +0 -1
- package/drizzle-sqlite/0003_status_entry_rules.sql +0 -1
- package/drizzle-sqlite/0004_run_external_session_ref.sql +0 -1
- package/drizzle-sqlite/README.md +0 -67
- package/drizzle-sqlite/meta/0000_snapshot.json +0 -946
- package/drizzle-sqlite/meta/0001_snapshot.json +0 -1054
- package/drizzle-sqlite/meta/0002_snapshot.json +0 -1061
- package/drizzle-sqlite/meta/0003_snapshot.json +0 -1068
- package/drizzle-sqlite/meta/0004_snapshot.json +0 -1075
- package/drizzle-sqlite/meta/_journal.json +0 -41
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import { a as BoardError, i as BOARD_TABLES_ADDED_WITHIN_EPOCH, n as resolveBoardMigrationsPath, t as hasMigrationJournal } from "./migrations-path-
|
|
1
|
+
import { a as BoardError, i as BOARD_TABLES_ADDED_WITHIN_EPOCH, n as resolveBoardMigrationsPath, t as hasMigrationJournal } from "./migrations-path-ClaYRNwk.mjs";
|
|
2
2
|
import { boardSqliteSchema, issueComments, issueFieldChanges, issueMilestones, issuePullRequests, issueRelations, issueRuns, issueStatuses, issues, kanbanMeta } from "./sqlite/schema.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { and, asc, desc, eq, getTableColumns, inArray, isNotNull, or, sql } from "drizzle-orm";
|
|
4
|
+
import { and, asc, desc, eq, getTableColumns, gte, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
|
|
5
5
|
import { getTableConfig } from "drizzle-orm/sqlite-core";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
9
|
+
//#region src/domain/fold-case.ts
|
|
10
|
+
/**
|
|
11
|
+
* Text as a search compares it: every letter lowercased by Unicode's rules, not
|
|
12
|
+
* only A–Z — `Über` and `über` are one word. What Postgres `ilike` does on
|
|
13
|
+
* either side of a board search; SQLite's own `like` folds ASCII alone, so a
|
|
14
|
+
* board file compares through this function instead (`kanbo_fold`).
|
|
15
|
+
*/
|
|
16
|
+
function foldCase(text) {
|
|
17
|
+
return text.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
9
20
|
//#region src/sqlite/transaction.ts
|
|
10
21
|
/**
|
|
11
22
|
* The better-sqlite3 handle Drizzle keeps beside the query builder. `drizzle()`
|
|
@@ -162,6 +173,82 @@ function selectRunProjection(database, scope) {
|
|
|
162
173
|
}));
|
|
163
174
|
}
|
|
164
175
|
/**
|
|
176
|
+
* One page of the cards a query picks, and how many it picks in all, in one
|
|
177
|
+
* statement: `count(*) over ()` is taken over every picked row before `limit`
|
|
178
|
+
* and `offset` cut the page out of them. Only a page past the last card — no
|
|
179
|
+
* row to carry the count — asks a second time.
|
|
180
|
+
*/
|
|
181
|
+
function selectCardPage(database, query) {
|
|
182
|
+
if (query.statusIds?.length === 0 || query.priorities?.length === 0) return {
|
|
183
|
+
cards: [],
|
|
184
|
+
total: 0
|
|
185
|
+
};
|
|
186
|
+
if (query.text !== void 0) registerFoldFunction(database);
|
|
187
|
+
const where = and(...cardQueryPredicates(query));
|
|
188
|
+
const rows = database.select({
|
|
189
|
+
...getTableColumns(issues),
|
|
190
|
+
pickedTotal: sql`count(*) over ()`
|
|
191
|
+
}).from(issues).where(where).orderBy(issues.order, desc(issues.createdAt), issues.id).limit(query.limit ?? Number.MAX_SAFE_INTEGER).offset(query.offset ?? 0).all();
|
|
192
|
+
if (rows.length === 0) return {
|
|
193
|
+
cards: [],
|
|
194
|
+
total: (query.offset ?? 0) > 0 ? database.select({ count: sql`count(*)` }).from(issues).where(where).get()?.count ?? 0 : 0
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
cards: rows.map(({ pickedTotal: _total, ...card }) => card),
|
|
198
|
+
total: rows[0].pickedTotal
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** The connections `kanbo_fold` is registered on already. */
|
|
202
|
+
const foldingConnections = /* @__PURE__ */ new WeakSet();
|
|
203
|
+
/**
|
|
204
|
+
* Teach the connection `kanbo_fold(text)`, the search's `foldCase`, once.
|
|
205
|
+
*
|
|
206
|
+
* SQLite's `like` and `lower()` fold A–Z and nothing else, so `über` would not
|
|
207
|
+
* find `Über` on a board file while Postgres `ilike` and the HTTP transport find
|
|
208
|
+
* it. The function is registered on the connection the statement runs on —
|
|
209
|
+
* the host may have opened it, not this package — and stays one statement.
|
|
210
|
+
*/
|
|
211
|
+
function registerFoldFunction(database) {
|
|
212
|
+
const connection = readSqliteConnection(database);
|
|
213
|
+
if (foldingConnections.has(connection)) return;
|
|
214
|
+
connection.function("kanbo_fold", { deterministic: true }, (value) => typeof value === "string" ? foldCase(value) : value);
|
|
215
|
+
foldingConnections.add(connection);
|
|
216
|
+
}
|
|
217
|
+
function cardQueryPredicates(query) {
|
|
218
|
+
const running = sql`${issues.id} in (select ${issueRuns.issueId} from ${issueRuns} where ${issueRuns.state} = 'running')`;
|
|
219
|
+
const text = foldCase(query.text ?? "");
|
|
220
|
+
return [
|
|
221
|
+
eq(issues.workspaceId, query.workspaceId),
|
|
222
|
+
query.statusIds === void 0 ? void 0 : inArray(issues.statusId, [...query.statusIds]),
|
|
223
|
+
query.waitingForPerson === void 0 ? void 0 : query.waitingForPerson ? eq(issues.waitingFor, "human") : isNull(issues.waitingFor),
|
|
224
|
+
query.parentIssueId === void 0 ? void 0 : eq(issues.parentIssueId, query.parentIssueId),
|
|
225
|
+
query.hasActiveRun === void 0 ? void 0 : query.hasActiveRun ? running : sql`not ${running}`,
|
|
226
|
+
query.returned === void 0 ? void 0 : query.returned ? returnedCard(running) : sql`not ${returnedCard(running)}`,
|
|
227
|
+
query.text === void 0 ? void 0 : sql`(instr(kanbo_fold(${issues.id}), ${text}) > 0 or instr(kanbo_fold(${issues.title}), ${text}) > 0 or instr(kanbo_fold(coalesce(${issues.description}, '')), ${text}) > 0)`,
|
|
228
|
+
query.updatedSince === void 0 ? void 0 : gte(issues.updatedAt, query.updatedSince),
|
|
229
|
+
...(query.labels ?? []).map((label) => sql`exists (select 1 from json_each(${issues.labels}) where value = ${label})`),
|
|
230
|
+
query.priorities === void 0 ? void 0 : inArray(issues.priority, [...query.priorities])
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A card a person sent back that nobody picked up again (`BoardCardQuery.returned`):
|
|
235
|
+
* not waiting, not running, and its last decision a return — no approval or
|
|
236
|
+
* return written after it, `rowid` breaking a tie inside one second the way
|
|
237
|
+
* `readApproval`'s comment order does.
|
|
238
|
+
*/
|
|
239
|
+
function returnedCard(running) {
|
|
240
|
+
return sql`(${issues.waitingFor} is null and not ${running} and exists (
|
|
241
|
+
select 1 from ${issueComments} as decision
|
|
242
|
+
where decision.issue_id = ${issues.id} and decision.author_kind = 'system.returned'
|
|
243
|
+
and not exists (
|
|
244
|
+
select 1 from ${issueComments} as later
|
|
245
|
+
where later.issue_id = decision.issue_id
|
|
246
|
+
and later.author_kind in ('system.approved', 'system.returned')
|
|
247
|
+
and (later.created_at > decision.created_at or (later.created_at = decision.created_at and later.rowid > decision.rowid))
|
|
248
|
+
)
|
|
249
|
+
))`;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
165
252
|
* Bind the store's statements to one SQLite handle. `resolveDatabase` is called
|
|
166
253
|
* per statement rather than captured, so the store follows a host that swaps its
|
|
167
254
|
* database out (a server that resets its infrastructure between tests, say).
|
|
@@ -173,6 +260,7 @@ function createSqliteBoardStoreCore(resolveDatabase) {
|
|
|
173
260
|
listAllInBoardOrder: () => resolveDatabase().select().from(issueStatuses).orderBy(issueStatuses.workspaceId, issueStatuses.order).all(),
|
|
174
261
|
listAll: () => resolveDatabase().select().from(issueStatuses).all(),
|
|
175
262
|
findById: (statusId) => resolveDatabase().select().from(issueStatuses).where(eq(issueStatuses.id, statusId)).get() ?? null,
|
|
263
|
+
lockById: (statusId) => resolveDatabase().select().from(issueStatuses).where(eq(issueStatuses.id, statusId)).get() ?? null,
|
|
176
264
|
findInWorkspace: (workspaceId, statusId) => resolveDatabase().select().from(issueStatuses).where(sql`${issueStatuses.workspaceId} = ${workspaceId} AND ${issueStatuses.id} = ${statusId}`).get() ?? null,
|
|
177
265
|
countByWorkspace: (workspaceId) => {
|
|
178
266
|
return resolveDatabase().select({ count: sql`count(*)` }).from(issueStatuses).where(eq(issueStatuses.workspaceId, workspaceId)).get()?.count ?? 0;
|
|
@@ -205,11 +293,17 @@ function createSqliteBoardStoreCore(resolveDatabase) {
|
|
|
205
293
|
issues: {
|
|
206
294
|
listInBoardOrder: (workspaceId) => {
|
|
207
295
|
const predicates = [workspaceId ? eq(issues.workspaceId, workspaceId) : void 0].filter((predicate) => predicate !== void 0);
|
|
208
|
-
return resolveDatabase().select().from(issues).where(predicates.length > 0 ? and(...predicates) : void 0).orderBy(issues.order, desc(issues.createdAt)).all();
|
|
296
|
+
return resolveDatabase().select().from(issues).where(predicates.length > 0 ? and(...predicates) : void 0).orderBy(issues.order, desc(issues.createdAt), issues.id).all();
|
|
209
297
|
},
|
|
210
298
|
listNewestFirst: () => resolveDatabase().select().from(issues).orderBy(desc(issues.createdAt)).all(),
|
|
211
299
|
listAll: () => resolveDatabase().select().from(issues).all(),
|
|
212
300
|
listByWorkspace: (workspaceId) => resolveDatabase().select().from(issues).where(eq(issues.workspaceId, workspaceId)).all(),
|
|
301
|
+
listPage: (query) => selectCardPage(resolveDatabase(), query),
|
|
302
|
+
countByMilestone: (workspaceId) => resolveDatabase().select({
|
|
303
|
+
milestoneId: sql`${issues.milestoneId}`,
|
|
304
|
+
statusId: issues.statusId,
|
|
305
|
+
count: sql`count(*)`
|
|
306
|
+
}).from(issues).where(and(eq(issues.workspaceId, workspaceId), isNotNull(issues.milestoneId))).groupBy(issues.milestoneId, issues.statusId).all(),
|
|
213
307
|
listByIds: (issueIds) => resolveDatabase().select().from(issues).where(inArray(issues.id, issueIds)).all(),
|
|
214
308
|
findById: (issueId) => resolveDatabase().select().from(issues).where(eq(issues.id, issueId)).get() ?? null,
|
|
215
309
|
findInWorkspace: (workspaceId, issueId) => resolveDatabase().select().from(issues).where(and(eq(issues.id, issueId), eq(issues.workspaceId, workspaceId))).get() ?? null,
|
|
@@ -248,6 +342,15 @@ function createSqliteBoardStoreCore(resolveDatabase) {
|
|
|
248
342
|
},
|
|
249
343
|
comments: {
|
|
250
344
|
listByIssue: (issueId) => resolveDatabase().select().from(issueComments).where(eq(issueComments.issueId, issueId)).orderBy(asc(issueComments.createdAt), COMMENT_INSERTION_ORDER).all(),
|
|
345
|
+
listLatestByIssues: (issueIds, authorKinds) => {
|
|
346
|
+
if (issueIds.length === 0 || authorKinds?.length === 0) return [];
|
|
347
|
+
const database = resolveDatabase();
|
|
348
|
+
const ranked = database.select({
|
|
349
|
+
id: issueComments.id,
|
|
350
|
+
commentRank: sql`row_number() over (partition by ${issueComments.issueId} order by ${issueComments.createdAt} desc, ${issueComments}.rowid desc)`.as("comment_rank")
|
|
351
|
+
}).from(issueComments).where(and(inArray(issueComments.issueId, [...issueIds]), authorKinds === void 0 ? void 0 : inArray(issueComments.authorKind, [...authorKinds]))).as("ranked");
|
|
352
|
+
return database.select(getTableColumns(issueComments)).from(issueComments).innerJoin(ranked, eq(ranked.id, issueComments.id)).where(eq(ranked.commentRank, 1)).all();
|
|
353
|
+
},
|
|
251
354
|
findById: (commentId) => resolveDatabase().select().from(issueComments).where(eq(issueComments.id, commentId)).get() ?? null,
|
|
252
355
|
findByDedupeKey: (issueId, dedupeKey) => resolveDatabase().select().from(issueComments).where(and(eq(issueComments.issueId, issueId), eq(issueComments.dedupeKey, dedupeKey))).get() ?? null,
|
|
253
356
|
listByDedupeKeyPrefix: (issueId, prefix) => resolveDatabase().select().from(issueComments).where(and(eq(issueComments.issueId, issueId), sql`substr(${issueComments.dedupeKey}, 1, ${prefix.length}) = ${prefix}`)).orderBy(asc(issueComments.createdAt), COMMENT_INSERTION_ORDER).all(),
|
|
@@ -354,6 +457,7 @@ function createSqliteBoardStore(options) {
|
|
|
354
457
|
listAllInBoardOrder: async () => core.statuses.listAllInBoardOrder(),
|
|
355
458
|
listAll: async () => core.statuses.listAll(),
|
|
356
459
|
findById: async (statusId) => core.statuses.findById(statusId),
|
|
460
|
+
lockById: async (statusId) => core.statuses.lockById(statusId),
|
|
357
461
|
findInWorkspace: async (workspaceId, statusId) => core.statuses.findInWorkspace(workspaceId, statusId),
|
|
358
462
|
countByWorkspace: async (workspaceId) => core.statuses.countByWorkspace(workspaceId),
|
|
359
463
|
create: async (values) => core.statuses.create(values),
|
|
@@ -375,6 +479,8 @@ function createSqliteBoardStore(options) {
|
|
|
375
479
|
listNewestFirst: async () => core.issues.listNewestFirst(),
|
|
376
480
|
listAll: async () => core.issues.listAll(),
|
|
377
481
|
listByWorkspace: async (workspaceId) => core.issues.listByWorkspace(workspaceId),
|
|
482
|
+
listPage: async (query) => core.issues.listPage(query),
|
|
483
|
+
countByMilestone: async (workspaceId) => core.issues.countByMilestone(workspaceId),
|
|
378
484
|
listByIds: async (issueIds) => core.issues.listByIds(issueIds),
|
|
379
485
|
findById: async (issueId) => core.issues.findById(issueId),
|
|
380
486
|
findInWorkspace: async (workspaceId, issueId) => core.issues.findInWorkspace(workspaceId, issueId),
|
|
@@ -391,6 +497,7 @@ function createSqliteBoardStore(options) {
|
|
|
391
497
|
},
|
|
392
498
|
comments: {
|
|
393
499
|
listByIssue: async (issueId) => core.comments.listByIssue(issueId),
|
|
500
|
+
listLatestByIssues: async (issueIds, authorKinds) => core.comments.listLatestByIssues(issueIds, authorKinds),
|
|
394
501
|
findById: async (commentId) => core.comments.findById(commentId),
|
|
395
502
|
findByDedupeKey: async (issueId, dedupeKey) => core.comments.findByDedupeKey(issueId, dedupeKey),
|
|
396
503
|
listByDedupeKeyPrefix: async (issueId, prefix) => core.comments.listByDedupeKeyPrefix(issueId, prefix),
|
|
@@ -448,25 +555,35 @@ function createSqliteBoardStore(options) {
|
|
|
448
555
|
};
|
|
449
556
|
return store;
|
|
450
557
|
}
|
|
558
|
+
/** The logger a board handle is opened with: the counter while one is on, otherwise none. */
|
|
559
|
+
function boardQueryLogger() {}
|
|
451
560
|
//#endregion
|
|
452
561
|
//#region src/sqlite/open-database.ts
|
|
453
562
|
const require = createRequire(import.meta.url);
|
|
454
563
|
/** The tables a board file must already carry for this package to read it. */
|
|
455
564
|
const REQUIRED_BOARD_TABLES = ["issue_runs", "kanban_meta"];
|
|
456
|
-
/** What a caller is told when the native module a board file needs is not installed. */
|
|
457
|
-
const BETTER_SQLITE3_MISSING_MESSAGE = "Install better-sqlite3 to use a board file";
|
|
458
565
|
/**
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
|
|
463
|
-
|
|
566
|
+
* What a caller is told when the native module a board file needs does not
|
|
567
|
+
* load: which Node, system and processor it was tried on — a prebuilt module
|
|
568
|
+
* exists for most, not all — what the loader said, and how to get it back.
|
|
569
|
+
*/
|
|
570
|
+
function betterSqlite3MissingMessage(cause) {
|
|
571
|
+
const said = cause instanceof Error ? cause.message.split("\n")[0] : String(cause);
|
|
572
|
+
return `Could not load better-sqlite3, which kanbo opens board files with, on Node ${process.version} (${process.platform}-${process.arch}): ${said}. Reinstall kanbo with npm install -g kanbo-cli, which brings it along; a program that uses kanbo as a library installs better-sqlite3 itself.`;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* `better-sqlite3` is an optional dependency (ruling 5): an install where it
|
|
576
|
+
* could not be built carries no native module, and a Postgres-only or
|
|
577
|
+
* capabilities-only use — or importing `kanbo/sqlite` for a type or for
|
|
578
|
+
* `runSqliteTransaction` — should not demand one. So the require happens here,
|
|
579
|
+
* the one place a board file is actually opened, rather than at the top of
|
|
580
|
+
* this module.
|
|
464
581
|
*/
|
|
465
582
|
function loadBetterSqlite3() {
|
|
466
583
|
try {
|
|
467
584
|
return require("better-sqlite3");
|
|
468
585
|
} catch (error) {
|
|
469
|
-
throw new Error(
|
|
586
|
+
throw new Error(betterSqlite3MissingMessage(error), { cause: error });
|
|
470
587
|
}
|
|
471
588
|
}
|
|
472
589
|
/**
|
|
@@ -479,7 +596,7 @@ function loadBetterSqlite3() {
|
|
|
479
596
|
* this module, `kanbo capabilities` and `kanbo/mcp` included, and the
|
|
480
597
|
* friendly message above becomes unreachable. A dynamic import keeps the whole
|
|
481
598
|
* chain behind the one call that actually opens a file, which is what makes the
|
|
482
|
-
* optional
|
|
599
|
+
* optional dependency honest. `createRequire` would be the smaller change and is the
|
|
483
600
|
* wrong one: it would load drizzle's CJS copy beside the ESM one this package
|
|
484
601
|
* already holds, and it would resolve from `node_modules`, which the packaged
|
|
485
602
|
* `dist/cli.cjs` deliberately does not have beside it.
|
|
@@ -512,7 +629,10 @@ async function openBoardDatabase(path) {
|
|
|
512
629
|
connection.pragma("busy_timeout = 5000");
|
|
513
630
|
connection.pragma("synchronous = NORMAL");
|
|
514
631
|
return {
|
|
515
|
-
database: drizzle(connection, {
|
|
632
|
+
database: drizzle(connection, {
|
|
633
|
+
schema: boardSqliteSchema,
|
|
634
|
+
logger: void 0
|
|
635
|
+
}),
|
|
516
636
|
connection,
|
|
517
637
|
close: () => connection.close()
|
|
518
638
|
};
|
|
@@ -651,6 +771,4 @@ function migrateBoardFile(database) {
|
|
|
651
771
|
migrate(database, { migrationsFolder: getBoardSqliteMigrationsPath() });
|
|
652
772
|
}
|
|
653
773
|
//#endregion
|
|
654
|
-
export { openBoardDatabase as a,
|
|
655
|
-
|
|
656
|
-
//# sourceMappingURL=migrate-BhYi9cXw.mjs.map
|
|
774
|
+
export { openBoardDatabase as a, boardQueryLogger as c, foldCase as d, markBoardFileOwnedByKanbo as i, createSqliteBoardStore as l, migrateBoardFile as n, readBoardFileContents as o, assertBoardSchema as r, readBoardFileOwner as s, getBoardSqliteMigrationsPath as t, runSqliteTransaction as u };
|
|
@@ -44,9 +44,9 @@ const BOARD_TABLES_ADDED_WITHIN_EPOCH = ["issue_pull_requests"];
|
|
|
44
44
|
* The folder is found relative to the file asking for it, because the same code
|
|
45
45
|
* runs from two layouts: from `src/<dialect>/` in a checkout, where the folder
|
|
46
46
|
* sits at the package root, and from `dist/cli.cjs` in an installed build, where
|
|
47
|
-
* the build has copied it in beside the bundle. The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* the build has copied it in beside the bundle. The npm tarball and the packaged
|
|
48
|
+
* command alike ship only that copy — `dist/` is the whole of it — so it is the
|
|
49
|
+
* only migrations folder an installed kanbo has.
|
|
50
50
|
*
|
|
51
51
|
* Kept apart from the migrators on purpose: this is a packaging question, and
|
|
52
52
|
* two dialects ask it about two folders. The caller passes its own `here`, so
|
|
@@ -57,7 +57,7 @@ const JOURNAL_FILE = join("meta", "_journal.json");
|
|
|
57
57
|
/**
|
|
58
58
|
* The first of the layouts that is actually there, as seen from `here`.
|
|
59
59
|
*
|
|
60
|
-
* Pure, and takes its own `exists`, so
|
|
60
|
+
* Pure, and takes its own `exists`, so every layout can be asked about on
|
|
61
61
|
* a machine that only has one of them. When none is there the source layout is
|
|
62
62
|
* handed back, so the migrator's complaint names the folder a developer is
|
|
63
63
|
* missing rather than one that only exists inside a packaged app.
|
|
@@ -66,7 +66,7 @@ function resolveBoardMigrationsPath(here, directory, exists) {
|
|
|
66
66
|
const candidates = [
|
|
67
67
|
join(here, "..", "..", directory),
|
|
68
68
|
join(here, directory),
|
|
69
|
-
join(here, "..",
|
|
69
|
+
join(here, "..", directory)
|
|
70
70
|
];
|
|
71
71
|
return candidates.find(exists) ?? candidates[0];
|
|
72
72
|
}
|
|
@@ -76,5 +76,3 @@ function hasMigrationJournal(folder) {
|
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
78
|
export { BoardError as a, BOARD_TABLES_ADDED_WITHIN_EPOCH as i, resolveBoardMigrationsPath as n, BOARD_SCHEMA_EPOCH as r, hasMigrationJournal as t };
|
|
79
|
-
|
|
80
|
-
//# sourceMappingURL=migrations-path-Ciwan537.mjs.map
|
|
@@ -44,9 +44,9 @@ const BOARD_TABLES_ADDED_WITHIN_EPOCH = ["issue_pull_requests"];
|
|
|
44
44
|
* The folder is found relative to the file asking for it, because the same code
|
|
45
45
|
* runs from two layouts: from `src/<dialect>/` in a checkout, where the folder
|
|
46
46
|
* sits at the package root, and from `dist/cli.cjs` in an installed build, where
|
|
47
|
-
* the build has copied it in beside the bundle. The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* the build has copied it in beside the bundle. The npm tarball and the packaged
|
|
48
|
+
* command alike ship only that copy — `dist/` is the whole of it — so it is the
|
|
49
|
+
* only migrations folder an installed kanbo has.
|
|
50
50
|
*
|
|
51
51
|
* Kept apart from the migrators on purpose: this is a packaging question, and
|
|
52
52
|
* two dialects ask it about two folders. The caller passes its own `here`, so
|
|
@@ -57,7 +57,7 @@ const JOURNAL_FILE = (0, node_path.join)("meta", "_journal.json");
|
|
|
57
57
|
/**
|
|
58
58
|
* The first of the layouts that is actually there, as seen from `here`.
|
|
59
59
|
*
|
|
60
|
-
* Pure, and takes its own `exists`, so
|
|
60
|
+
* Pure, and takes its own `exists`, so every layout can be asked about on
|
|
61
61
|
* a machine that only has one of them. When none is there the source layout is
|
|
62
62
|
* handed back, so the migrator's complaint names the folder a developer is
|
|
63
63
|
* missing rather than one that only exists inside a packaged app.
|
|
@@ -66,7 +66,7 @@ function resolveBoardMigrationsPath(here, directory, exists) {
|
|
|
66
66
|
const candidates = [
|
|
67
67
|
(0, node_path.join)(here, "..", "..", directory),
|
|
68
68
|
(0, node_path.join)(here, directory),
|
|
69
|
-
(0, node_path.join)(here, "..",
|
|
69
|
+
(0, node_path.join)(here, "..", directory)
|
|
70
70
|
];
|
|
71
71
|
return candidates.find(exists) ?? candidates[0];
|
|
72
72
|
}
|
|
@@ -105,5 +105,3 @@ Object.defineProperty(exports, "resolveBoardMigrationsPath", {
|
|
|
105
105
|
return resolveBoardMigrationsPath;
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
|
-
|
|
109
|
-
//# sourceMappingURL=migrations-path-CZxVOgeD.cjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_migrations_path = require("../migrations-path-
|
|
3
|
-
const require_roles_sql = require("../roles.sql-
|
|
2
|
+
const require_migrations_path = require("../migrations-path-DBCE17dT.cjs");
|
|
3
|
+
const require_roles_sql = require("../roles.sql-E58x7mLS.cjs");
|
|
4
4
|
let drizzle_orm = require("drizzle-orm");
|
|
5
5
|
let drizzle_orm_postgres_js_migrator = require("drizzle-orm/postgres-js/migrator");
|
|
6
6
|
let drizzle_orm_pglite_migrator = require("drizzle-orm/pglite/migrator");
|
|
@@ -52,5 +52,3 @@ exports.issueStatuses = require_roles_sql.issueStatuses;
|
|
|
52
52
|
exports.issues = require_roles_sql.issues;
|
|
53
53
|
exports.kanbanMeta = require_roles_sql.kanbanMeta;
|
|
54
54
|
exports.migrateBoardDatabase = migrateBoardDatabase;
|
|
55
|
-
|
|
56
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as BoardStore } from "../board-store-Dx-mw2I6.cjs";
|
|
2
2
|
import { t as BOARD_SCHEMA_EPOCH } from "../schema-epoch-ZyumVL_A.cjs";
|
|
3
3
|
import { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
4
4
|
import { PgliteDatabase } from "drizzle-orm/pglite";
|
|
@@ -3399,5 +3399,4 @@ export declare const boardPostgresSchema: {
|
|
|
3399
3399
|
/** The table names a Postgres board must carry, for the guard that refuses a database without them. */
|
|
3400
3400
|
export declare const BOARD_POSTGRES_TABLES: readonly ['issue_statuses', 'issue_milestones', 'issues', 'issue_comments', 'issue_runs', 'issue_relations', 'issue_field_changes', 'issue_pull_requests', 'kanban_meta'];
|
|
3401
3401
|
//#endregion
|
|
3402
|
-
export { BOARD_SCHEMA_EPOCH, type BoardPostgresDatabase, type MigratablePostgresDatabase };
|
|
3403
|
-
//# sourceMappingURL=index.d.cts.map
|
|
3402
|
+
export { BOARD_SCHEMA_EPOCH, type BoardPostgresDatabase, type MigratablePostgresDatabase };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as BoardStore } from "../board-store-D_Mpfb84.mjs";
|
|
2
2
|
import { t as BOARD_SCHEMA_EPOCH } from "../schema-epoch-ZyumVL_A.mjs";
|
|
3
3
|
import { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
4
4
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
@@ -3399,5 +3399,4 @@ export declare const boardPostgresSchema: {
|
|
|
3399
3399
|
/** The table names a Postgres board must carry, for the guard that refuses a database without them. */
|
|
3400
3400
|
export declare const BOARD_POSTGRES_TABLES: readonly ['issue_statuses', 'issue_milestones', 'issues', 'issue_comments', 'issue_runs', 'issue_relations', 'issue_field_changes', 'issue_pull_requests', 'kanban_meta'];
|
|
3401
3401
|
//#endregion
|
|
3402
|
-
export { BOARD_SCHEMA_EPOCH, type BoardPostgresDatabase, type MigratablePostgresDatabase };
|
|
3403
|
-
//# sourceMappingURL=index.d.mts.map
|
|
3402
|
+
export { BOARD_SCHEMA_EPOCH, type BoardPostgresDatabase, type MigratablePostgresDatabase };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as BOARD_SCHEMA_EPOCH } from "../migrations-path-
|
|
2
|
-
import { _ as issueStatuses, a as BOARD_REFUSAL_ERRCODE, c as getBoardMigrationsPath, d as boardPostgresSchema, f as issueComments, g as issueRuns, h as issueRelations, i as BOARD_PERSON_ROLE, l as createPostgresBoardStore, m as issueMilestones, n as AGENT_WAITING_REFUSAL, o as BOARD_ROLE_SQL, p as issueFieldChanges, r as BOARD_AGENT_ROLE, s as assertBoardSchema, t as AGENT_APPROVAL_COMMENT_REFUSAL, u as BOARD_POSTGRES_TABLES, v as issues, y as kanbanMeta } from "../roles.sql-
|
|
1
|
+
import { r as BOARD_SCHEMA_EPOCH } from "../migrations-path-ClaYRNwk.mjs";
|
|
2
|
+
import { _ as issueStatuses, a as BOARD_REFUSAL_ERRCODE, c as getBoardMigrationsPath, d as boardPostgresSchema, f as issueComments, g as issueRuns, h as issueRelations, i as BOARD_PERSON_ROLE, l as createPostgresBoardStore, m as issueMilestones, n as AGENT_WAITING_REFUSAL, o as BOARD_ROLE_SQL, p as issueFieldChanges, r as BOARD_AGENT_ROLE, s as assertBoardSchema, t as AGENT_APPROVAL_COMMENT_REFUSAL, u as BOARD_POSTGRES_TABLES, v as issues, y as kanbanMeta } from "../roles.sql-BoOZSkBp.mjs";
|
|
3
3
|
import { entityKind } from "drizzle-orm";
|
|
4
4
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
5
5
|
import { migrate as migrate$1 } from "drizzle-orm/pglite/migrator";
|
|
@@ -31,5 +31,3 @@ function isPgliteDatabase(database) {
|
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
33
|
export { AGENT_APPROVAL_COMMENT_REFUSAL, AGENT_WAITING_REFUSAL, BOARD_AGENT_ROLE, BOARD_PERSON_ROLE, BOARD_POSTGRES_TABLES, BOARD_REFUSAL_ERRCODE, BOARD_ROLE_SQL, BOARD_SCHEMA_EPOCH, assertBoardSchema, boardPostgresSchema, createPostgresBoardStore, getBoardMigrationsPath, issueComments, issueFieldChanges, issueMilestones, issueRelations, issueRuns, issueStatuses, issues, kanbanMeta, migrateBoardDatabase };
|
|
34
|
-
|
|
35
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { a as BoardError, i as BOARD_TABLES_ADDED_WITHIN_EPOCH, n as resolveBoardMigrationsPath, t as hasMigrationJournal } from "./migrations-path-
|
|
2
|
-
import { and, asc, desc, eq, getTableColumns, inArray, isNotNull, or, sql } from "drizzle-orm";
|
|
1
|
+
import { a as BoardError, i as BOARD_TABLES_ADDED_WITHIN_EPOCH, n as resolveBoardMigrationsPath, t as hasMigrationJournal } from "./migrations-path-ClaYRNwk.mjs";
|
|
2
|
+
import { and, asc, desc, eq, getTableColumns, gte, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
|
|
3
3
|
import { bigint, bigserial, getTableConfig, index, integer, pgSchema, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/domain/like-pattern.ts
|
|
7
|
+
/**
|
|
8
|
+
* A `like` pattern that finds `text` anywhere in a value, the text matched as
|
|
9
|
+
* written: `%`, `_` and the escape character itself are escaped with `\`, the
|
|
10
|
+
* character a statement names in `escape '\'`.
|
|
11
|
+
*/
|
|
12
|
+
function containsLikePattern(text) {
|
|
13
|
+
return `%${text.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
6
16
|
//#region src/postgres/schema.ts
|
|
7
17
|
/**
|
|
8
18
|
* The board, as a Postgres database holds it.
|
|
@@ -348,6 +358,71 @@ async function selectRunProjection(database, scope) {
|
|
|
348
358
|
}));
|
|
349
359
|
}
|
|
350
360
|
/**
|
|
361
|
+
* One page of the cards a query picks, and how many it picks in all, in one
|
|
362
|
+
* statement — the SQLite store's read, with the count cast to `int` (Postgres
|
|
363
|
+
* counts in `bigint`) and text matched with `ilike`, which folds every letter.
|
|
364
|
+
*/
|
|
365
|
+
async function selectCardPage(database, query) {
|
|
366
|
+
if (query.statusIds?.length === 0 || query.priorities?.length === 0) return {
|
|
367
|
+
cards: [],
|
|
368
|
+
total: 0
|
|
369
|
+
};
|
|
370
|
+
const where = and(...cardQueryPredicates(query));
|
|
371
|
+
const page = database.select({
|
|
372
|
+
...getTableColumns(issues),
|
|
373
|
+
pickedTotal: sql`cast(count(*) over () as int)`
|
|
374
|
+
}).from(issues).where(where).orderBy(issues.order, desc(issues.createdAt), issues.id).offset(query.offset ?? 0).$dynamic();
|
|
375
|
+
const rows = await (query.limit === void 0 ? page : page.limit(query.limit));
|
|
376
|
+
if (rows.length === 0) {
|
|
377
|
+
if ((query.offset ?? 0) === 0) return {
|
|
378
|
+
cards: [],
|
|
379
|
+
total: 0
|
|
380
|
+
};
|
|
381
|
+
const [row] = await database.select({ count: sql`cast(count(*) as int)` }).from(issues).where(where);
|
|
382
|
+
return {
|
|
383
|
+
cards: [],
|
|
384
|
+
total: row?.count ?? 0
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
cards: rows.map(({ pickedTotal: _total, ...card }) => card),
|
|
389
|
+
total: rows[0].pickedTotal
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function cardQueryPredicates(query) {
|
|
393
|
+
const running = sql`${issues.id} in (select ${issueRuns.issueId} from ${issueRuns} where ${issueRuns.state} = 'running')`;
|
|
394
|
+
const pattern = containsLikePattern(query.text ?? "");
|
|
395
|
+
return [
|
|
396
|
+
eq(issues.workspaceId, query.workspaceId),
|
|
397
|
+
query.statusIds === void 0 ? void 0 : inArray(issues.statusId, [...query.statusIds]),
|
|
398
|
+
query.waitingForPerson === void 0 ? void 0 : query.waitingForPerson ? eq(issues.waitingFor, "human") : isNull(issues.waitingFor),
|
|
399
|
+
query.parentIssueId === void 0 ? void 0 : eq(issues.parentIssueId, query.parentIssueId),
|
|
400
|
+
query.hasActiveRun === void 0 ? void 0 : query.hasActiveRun ? running : sql`not ${running}`,
|
|
401
|
+
query.returned === void 0 ? void 0 : query.returned ? returnedCard(running) : sql`not ${returnedCard(running)}`,
|
|
402
|
+
query.text === void 0 ? void 0 : sql`(${issues.id} ilike ${pattern} escape '\\' or ${issues.title} ilike ${pattern} escape '\\' or coalesce(${issues.description}, '') ilike ${pattern} escape '\\')`,
|
|
403
|
+
query.updatedSince === void 0 ? void 0 : gte(issues.updatedAt, query.updatedSince),
|
|
404
|
+
...(query.labels ?? []).map((label) => sql`cast(${issues.labels} as jsonb) @> cast(${JSON.stringify([label])} as jsonb)`),
|
|
405
|
+
query.priorities === void 0 ? void 0 : inArray(issues.priority, [...query.priorities])
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* A card a person sent back that nobody picked up again (`BoardCardQuery.returned`):
|
|
410
|
+
* not waiting, not running, and its last decision a return — `seq` breaking a
|
|
411
|
+
* tie inside one second where SQLite reads `rowid`.
|
|
412
|
+
*/
|
|
413
|
+
function returnedCard(running) {
|
|
414
|
+
return sql`(${issues.waitingFor} is null and not ${running} and exists (
|
|
415
|
+
select 1 from ${issueComments} as decision
|
|
416
|
+
where decision.issue_id = ${issues.id} and decision.author_kind = 'system.returned'
|
|
417
|
+
and not exists (
|
|
418
|
+
select 1 from ${issueComments} as later
|
|
419
|
+
where later.issue_id = decision.issue_id
|
|
420
|
+
and later.author_kind in ('system.approved', 'system.returned')
|
|
421
|
+
and (later.created_at > decision.created_at or (later.created_at = decision.created_at and later.seq > decision.seq))
|
|
422
|
+
)
|
|
423
|
+
))`;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
351
426
|
* The board's statements against one Postgres handle — a connection, or a
|
|
352
427
|
* transaction on one.
|
|
353
428
|
*
|
|
@@ -365,6 +440,10 @@ function createStoreOver(database) {
|
|
|
365
440
|
const [row] = await database.select().from(issueStatuses).where(eq(issueStatuses.id, statusId)).limit(1);
|
|
366
441
|
return row ?? null;
|
|
367
442
|
},
|
|
443
|
+
lockById: async (statusId) => {
|
|
444
|
+
const [row] = await database.select().from(issueStatuses).where(eq(issueStatuses.id, statusId)).limit(1).for("update");
|
|
445
|
+
return row ?? null;
|
|
446
|
+
},
|
|
368
447
|
findInWorkspace: async (workspaceId, statusId) => {
|
|
369
448
|
const [row] = await database.select().from(issueStatuses).where(and(eq(issueStatuses.workspaceId, workspaceId), eq(issueStatuses.id, statusId))).limit(1);
|
|
370
449
|
return row ?? null;
|
|
@@ -408,10 +487,16 @@ function createStoreOver(database) {
|
|
|
408
487
|
}
|
|
409
488
|
},
|
|
410
489
|
issues: {
|
|
411
|
-
listInBoardOrder: async (workspaceId) => await database.select().from(issues).where(workspaceId ? eq(issues.workspaceId, workspaceId) : void 0).orderBy(issues.order, desc(issues.createdAt)),
|
|
490
|
+
listInBoardOrder: async (workspaceId) => await database.select().from(issues).where(workspaceId ? eq(issues.workspaceId, workspaceId) : void 0).orderBy(issues.order, desc(issues.createdAt), issues.id),
|
|
412
491
|
listNewestFirst: async () => await database.select().from(issues).orderBy(desc(issues.createdAt)),
|
|
413
492
|
listAll: async () => await database.select().from(issues),
|
|
414
493
|
listByWorkspace: async (workspaceId) => await database.select().from(issues).where(eq(issues.workspaceId, workspaceId)),
|
|
494
|
+
listPage: async (query) => await selectCardPage(database, query),
|
|
495
|
+
countByMilestone: async (workspaceId) => await database.select({
|
|
496
|
+
milestoneId: sql`${issues.milestoneId}`,
|
|
497
|
+
statusId: issues.statusId,
|
|
498
|
+
count: sql`cast(count(*) as int)`
|
|
499
|
+
}).from(issues).where(and(eq(issues.workspaceId, workspaceId), isNotNull(issues.milestoneId))).groupBy(issues.milestoneId, issues.statusId),
|
|
415
500
|
listByIds: async (issueIds) => await database.select().from(issues).where(inArray(issues.id, issueIds)),
|
|
416
501
|
findById: async (issueId) => {
|
|
417
502
|
const [row] = await database.select().from(issues).where(eq(issues.id, issueId)).limit(1);
|
|
@@ -481,6 +566,14 @@ function createStoreOver(database) {
|
|
|
481
566
|
},
|
|
482
567
|
comments: {
|
|
483
568
|
listByIssue: async (issueId) => (await database.select().from(issueComments).where(eq(issueComments.issueId, issueId)).orderBy(asc(issueComments.createdAt), asc(issueComments.seq))).map(toIssueComment),
|
|
569
|
+
listLatestByIssues: async (issueIds, authorKinds) => {
|
|
570
|
+
if (issueIds.length === 0 || authorKinds?.length === 0) return [];
|
|
571
|
+
const ranked = database.select({
|
|
572
|
+
id: issueComments.id,
|
|
573
|
+
commentRank: sql`cast(row_number() over (partition by ${issueComments.issueId} order by ${issueComments.createdAt} desc, ${issueComments.seq} desc) as int)`.as("comment_rank")
|
|
574
|
+
}).from(issueComments).where(and(inArray(issueComments.issueId, [...issueIds]), authorKinds === void 0 ? void 0 : inArray(issueComments.authorKind, [...authorKinds]))).as("ranked");
|
|
575
|
+
return (await database.select(getTableColumns(issueComments)).from(issueComments).innerJoin(ranked, eq(ranked.id, issueComments.id)).where(eq(ranked.commentRank, 1))).map(toIssueComment);
|
|
576
|
+
},
|
|
484
577
|
findById: async (commentId) => {
|
|
485
578
|
const [row] = await database.select().from(issueComments).where(eq(issueComments.id, commentId)).limit(1);
|
|
486
579
|
return row ? toIssueComment(row) : null;
|
|
@@ -916,5 +1009,3 @@ create trigger kanban_guard_approval_comment_updates
|
|
|
916
1009
|
`;
|
|
917
1010
|
//#endregion
|
|
918
1011
|
export { issueStatuses as _, BOARD_REFUSAL_ERRCODE as a, getBoardMigrationsPath as c, boardPostgresSchema as d, issueComments as f, issueRuns as g, issueRelations as h, BOARD_PERSON_ROLE as i, createPostgresBoardStore as l, issueMilestones as m, AGENT_WAITING_REFUSAL as n, BOARD_ROLE_SQL as o, issueFieldChanges as p, BOARD_AGENT_ROLE as r, assertBoardSchema as s, AGENT_APPROVAL_COMMENT_REFUSAL as t, BOARD_POSTGRES_TABLES as u, issues as v, kanbanMeta as y };
|
|
919
|
-
|
|
920
|
-
//# sourceMappingURL=roles.sql-CvyCGt38.mjs.map
|