@maudecode/kanvil 0.6.1 → 0.7.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/apps/server/dist/{auth-CFjetlB4.mjs → auth-D43XyHdK.mjs} +227 -71
- package/apps/server/dist/client.mjs +2 -2
- package/apps/server/dist/{config-BQw6mPzE.mjs → config-CQVdVhXv.mjs} +1 -1
- package/apps/server/dist/index.mjs +161 -44
- package/apps/server/dist/migrate.mjs +1 -1
- package/apps/server/dist/recover.mjs +3 -3
- package/apps/server/dist/{src-BaTNDJB7.mjs → src-D70GmZ5R.mjs} +83 -26
- package/apps/server/drizzle/0040_upload_urls.sql +20 -0
- package/apps/server/drizzle/0041_project_repositories.sql +2 -0
- package/apps/server/drizzle/meta/0040_snapshot.json +7170 -0
- package/apps/server/drizzle/meta/0041_snapshot.json +7183 -0
- package/apps/server/drizzle/meta/_journal.json +14 -0
- package/apps/web/dist/assets/{CompositeItem-Dotj8JZL.js → CompositeItem-DLTRsada.js} +1 -1
- package/apps/web/dist/assets/{Trans-CE4EAAY1.js → Trans-DqX-qhvQ.js} +1 -1
- package/apps/web/dist/assets/admin-settings-BQzdZII5.js +1 -0
- package/apps/web/dist/assets/{archive-restore-BMtgjFUu.js → archive-restore-BO1qng5Y.js} +1 -1
- package/apps/web/dist/assets/az-AZ-SXqDBPA2.js +1 -0
- package/apps/web/dist/assets/{az-B-DxMc__.js → az-CEW7-a7J.js} +1 -1
- package/apps/web/dist/assets/configuration-D0zvwrXt.js +1 -0
- package/apps/web/dist/assets/{corner-left-up-O4OsTW-L.js → corner-left-up-Be7Z9Uhf.js} +1 -1
- package/apps/web/dist/assets/{de-Btbkq-aX.js → de-B-oGVMYL.js} +1 -1
- package/apps/web/dist/assets/de-DE-CB7aQ6b-.js +1 -0
- package/apps/web/dist/assets/default-CCI1vZxa.js +56 -0
- package/apps/web/dist/assets/delivery-settings-BrwSrFIN.js +1 -0
- package/apps/web/dist/assets/{el-DBVEX16k.js → el-Cp69kezM.js} +1 -1
- package/apps/web/dist/assets/el-GR-D1ntYXIt.js +1 -0
- package/apps/web/dist/assets/{ellipsis-CvEPDi9V.js → ellipsis-B0AE8ax3.js} +1 -1
- package/apps/web/dist/assets/{es-CMJoxZBk.js → es-CwOMVSTx.js} +1 -1
- package/apps/web/dist/assets/es-ES-CllsaxcB.js +1 -0
- package/apps/web/dist/assets/{forms-DbfnnPg_.js → forms-CkVpTx29.js} +1 -1
- package/apps/web/dist/assets/{fr-OyEmYoRn.js → fr-Cy4co9ic.js} +1 -1
- package/apps/web/dist/assets/fr-FR-DxTw6LgI.js +1 -0
- package/apps/web/dist/assets/{hi-CIgXSseB.js → hi-DbO9TKwg.js} +1 -1
- package/apps/web/dist/assets/hi-IN-CaQga-0l.js +1 -0
- package/apps/web/dist/assets/{highlight-B4K3yEEh.js → highlight-zMg1BK-r.js} +1 -1
- package/apps/web/dist/assets/{id-WV2oSAPB.js → id-B6NKsmCm.js} +1 -1
- package/apps/web/dist/assets/id-ID-DBfwAAqU.js +1 -0
- package/apps/web/dist/assets/index-CZuFd4LM.js +17 -0
- package/apps/web/dist/assets/index-DPT5LAs1.css +2 -0
- package/apps/web/dist/assets/{invitations-BTB8f2Xl.js → invitations-Dp40BfHj.js} +1 -1
- package/apps/web/dist/assets/it-IT-BckuH9Nl.js +1 -0
- package/apps/web/dist/assets/{it-BWSrRqus.js → it-ilGYCq2T.js} +1 -1
- package/apps/web/dist/assets/{item-HmFuRZet.js → item-DQR89bKm.js} +1 -1
- package/apps/web/dist/assets/{ja-BtOHWvUJ.js → ja-D-hJRTgP.js} +1 -1
- package/apps/web/dist/assets/ja-JP-BAGgJKZ4.js +1 -0
- package/apps/web/dist/assets/ko-KR-srIEsTP5.js +1 -0
- package/apps/web/dist/assets/{ko-DAryK0HN.js → ko-ajkkoJ_d.js} +1 -1
- package/apps/web/dist/assets/{label-settings-ssfEX2uR.js → label-settings-ChnKkA97.js} +1 -1
- package/apps/web/dist/assets/{markdown-blocks-B1kO6iku.js → markdown-blocks-CpzrwyQG.js} +1 -1
- package/apps/web/dist/assets/{member-directory-BA4ZT1qU.js → member-directory-XK0ZEGUq.js} +1 -1
- package/apps/web/dist/assets/{mk-DBbiGUk5.js → mk-C_sMyEHX.js} +1 -1
- package/apps/web/dist/assets/mk-MK-BTa1NMAF.js +1 -0
- package/apps/web/dist/assets/{nl-D4V-8WdF.js → nl-D9-P4UPR.js} +1 -1
- package/apps/web/dist/assets/nl-NL-CQduhDqs.js +1 -0
- package/apps/web/dist/assets/{pl-C--QrxO0.js → pl-CiQM7fSB.js} +1 -1
- package/apps/web/dist/assets/pl-PL-BdZMf7Ni.js +1 -0
- package/apps/web/dist/assets/project-lifecycle-VYecJuiP.js +1 -0
- package/apps/web/dist/assets/project-settings-Bs6YEpo9.js +1 -0
- package/apps/web/dist/assets/{pt-BR-DvFVKpmO.js → pt-BR-CP7dhuqF.js} +1 -1
- package/apps/web/dist/assets/pt-BR-DZEXJNHg.js +1 -0
- package/apps/web/dist/assets/public-project-DUfWDao9.js +1 -0
- package/apps/web/dist/assets/{rotate-ccw-CB36Zu3L.js → rotate-ccw-Cxh9ec2s.js} +1 -1
- package/apps/web/dist/assets/{ru-UmbPUOka.js → ru-DxJ6vF4E.js} +1 -1
- package/apps/web/dist/assets/ru-RU-CZKEe7P8.js +1 -0
- package/apps/web/dist/assets/{search-Cvetr1OK.js → search-DfbsqO7M.js} +6 -6
- package/apps/web/dist/assets/{send-CJDct4Oj.js → send-DrJl0BKm.js} +1 -1
- package/apps/web/dist/assets/settings-BN8uIiBG.js +1 -0
- package/apps/web/dist/assets/{sliders-horizontal-dmqWngxy.js → sliders-horizontal-DM47MLe7.js} +1 -1
- package/apps/web/dist/assets/{switch-BtScTpP9.js → switch-CJTVGtE9.js} +1 -1
- package/apps/web/dist/assets/{table-byFokzo-.js → table-BLLjFPWD.js} +1 -1
- package/apps/web/dist/assets/{ticket-detail-C3YA3Hzc.js → ticket-detail-BTy_3JeU.js} +2 -2
- package/apps/web/dist/assets/ticket-editor-Bb_lyxnJ.js +2 -0
- package/apps/web/dist/assets/{ticket-list-BXUorfmj.js → ticket-list-Botp5qf5.js} +5 -5
- package/apps/web/dist/assets/ticket-list-controls-Dmkno4pm.js +1 -0
- package/apps/web/dist/assets/{tr-CNpgQ4ka.js → tr-9IkdRLIk.js} +1 -1
- package/apps/web/dist/assets/tr-TR-Y8wBLO8I.js +1 -0
- package/apps/web/dist/assets/{uk-MQoTXdpk.js → uk-Btampsdw.js} +1 -1
- package/apps/web/dist/assets/uk-UA-CVpasGUS.js +1 -0
- package/apps/web/dist/assets/{upload-D1x_ib3B.js → upload-HARy4VC_.js} +1 -1
- package/apps/web/dist/assets/{useQueries-BnlNCoy4.js → useQueries-CTeEF--U.js} +1 -1
- package/apps/web/dist/assets/{vi-CEw4gTWc.js → vi-BVZzOn_n.js} +1 -1
- package/apps/web/dist/assets/vi-VN-BMy-iJD-.js +1 -0
- package/apps/web/dist/assets/{visual-editor-CpdMRn3e.js → visual-editor-DKBPjqV-.js} +1 -1
- package/apps/web/dist/assets/{workspace-settings-BickBtZx.js → workspace-settings-DqzBNw60.js} +1 -1
- package/apps/web/dist/assets/{zh-CN-ChzoXkfr.js → zh-CN-BoUvF2do.js} +1 -1
- package/apps/web/dist/assets/zh-CN-Cds_v0R8.js +1 -0
- package/apps/web/dist/assets/{zh-TW-bWBKARrK.js → zh-TW-CEIDGTWe.js} +1 -1
- package/apps/web/dist/assets/zh-TW-DCCYakXz.js +1 -0
- package/apps/web/dist/index.html +8 -8
- package/docs/api.md +33 -3
- package/docs/cli.md +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/admin-settings-CO7nnD9T.js +0 -1
- package/apps/web/dist/assets/az-AZ-R8_HFWJr.js +0 -1
- package/apps/web/dist/assets/configuration-Bnt72HRu.js +0 -1
- package/apps/web/dist/assets/de-DE-BHgMj19Y.js +0 -1
- package/apps/web/dist/assets/default-DqsN66dq.js +0 -56
- package/apps/web/dist/assets/delivery-settings-Cji0N0m0.js +0 -1
- package/apps/web/dist/assets/el-GR-CQFGM5Sr.js +0 -1
- package/apps/web/dist/assets/es-ES-DZkcRVA4.js +0 -1
- package/apps/web/dist/assets/fr-FR-CRPAezXU.js +0 -1
- package/apps/web/dist/assets/hi-IN-2YFE4UQk.js +0 -1
- package/apps/web/dist/assets/id-ID-B-J0_QIW.js +0 -1
- package/apps/web/dist/assets/index-BAnwOrh1.css +0 -2
- package/apps/web/dist/assets/index-nltuyQtq.js +0 -17
- package/apps/web/dist/assets/it-IT-CJ0HYggO.js +0 -1
- package/apps/web/dist/assets/ja-JP-DDQS7D7b.js +0 -1
- package/apps/web/dist/assets/ko-KR-CkpoewEa.js +0 -1
- package/apps/web/dist/assets/mk-MK-C0RhS6XR.js +0 -1
- package/apps/web/dist/assets/nl-NL-BSKqlzcL.js +0 -1
- package/apps/web/dist/assets/pl-PL-CHz38SiU.js +0 -1
- package/apps/web/dist/assets/project-lifecycle-COZb9FqM.js +0 -1
- package/apps/web/dist/assets/project-settings-DwmTm9KA.js +0 -1
- package/apps/web/dist/assets/pt-BR-BotWHRiO.js +0 -1
- package/apps/web/dist/assets/public-project-B3hEkeCF.js +0 -1
- package/apps/web/dist/assets/ru-RU-DtOwNna6.js +0 -1
- package/apps/web/dist/assets/settings-3IXm9lJ-.js +0 -1
- package/apps/web/dist/assets/ticket-editor-CZnZQMG1.js +0 -2
- package/apps/web/dist/assets/ticket-list-controls-BxGKCZMo.js +0 -1
- package/apps/web/dist/assets/tr-TR-CdG2alU2.js +0 -1
- package/apps/web/dist/assets/uk-UA-Ceu5gorw.js +0 -1
- package/apps/web/dist/assets/vi-VN-CxshgV4W.js +0 -1
- package/apps/web/dist/assets/zh-CN-D7Yrjhd3.js +0 -1
- package/apps/web/dist/assets/zh-TW-DyF38kqx.js +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { a as auth_schema_exports, c as oauthConsent, d as user, i as apikey, l as oauthRefreshToken, o as oauthAccessToken, r as account, s as oauthClient, u as session } from "./config-
|
|
2
|
-
import { $ as
|
|
3
|
-
import { and, asc, count, desc, eq, getTableColumns, gt,
|
|
1
|
+
import { a as auth_schema_exports, c as oauthConsent, d as user, i as apikey, l as oauthRefreshToken, o as oauthAccessToken, r as account, s as oauthClient, u as session } from "./config-CQVdVhXv.mjs";
|
|
2
|
+
import { $ as parseCustomFieldFilter, A as filterList, At as ticketImportSchema, Bt as instanceUpdateSchema, C as destinationKinds, Ct as stagedUploadLifetime, D as eventLabels, E as eventCategories, Et as states, F as importSecretFields, Ft as workflowOrder, Gt as searchSettingsSchema, I as invitationStatuses, It as workspaceSchema, L as isHexColor, M as hasAdminRole, Mt as ticketSchema, Nt as ticketSummarySchema, O as eventSchema, Ot as ticketExportFormat, Pt as uploadUrlLifetime, Q as operations, Rt as authenticationPolicySchema, S as destinationEventKeys, St as snapshotSchema, Tt as stateLabels, U as mentionScheme, Ut as s3SettingsSchema, V as isSeededRole, W as normalizeRemote, Wt as searchAuthentication, Z as operationPermission, _ as defaultOrder, _t as roles, a as channelRouteAllows, at as priorities, b as deliverySecretFields, bt as seededPermissions, c as claimSchema, ct as publicOperations, d as credentialProjectsSchema, dt as repositoryProviderDefinitions, et as parseOutput, f as customFieldFilterTokens, ft as repositoryProviders, g as defaultClaimMaxLeaseSeconds, gt as roleLabels, h as customFieldValueSchema, ht as roleCategory, i as channelKinds, it as permissions, j as formatEvent, jt as ticketPredicates, k as filterKey, kt as ticketFilter, l as closedStates, m as customFieldValue, mt as resourceProvider, n as attachmentLimit, nt as permissionLabels, ot as priorityLabels, p as customFieldSchema, pt as repositorySecretFields, r as attachmentSchema, rt as permissionScope, st as projectSchema, t as ApiError, tt as parsePublicOutput, ut as remoteKinds, v as defaultWorkflow, vt as scopes, w as difficulties, wt as stagedUploadSchema, x as deliverySettingsSchema, xt as signInMethodLabels, y as deletedAccountName, yt as searchResultTypes, zt as brokerSettingsSchema } from "./src-D70GmZ5R.mjs";
|
|
3
|
+
import { and, arrayContains, arrayOverlaps, asc, count, desc, eq, getTableColumns, gt, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
|
|
4
4
|
import { alias, bigint, boolean, check, customType, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unionAll, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { decodeJwt } from "jose";
|
|
7
7
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "node:crypto";
|
|
8
8
|
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
10
|
import { icons } from "@iconify-json/lucide";
|
|
10
11
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
11
12
|
import { Expo } from "expo-server-sdk";
|
|
@@ -136,6 +137,7 @@ const projects = pgTable("projects", {
|
|
|
136
137
|
key: text("key").notNull().unique(),
|
|
137
138
|
name: text("name").notNull(),
|
|
138
139
|
description: text("description").notNull().default(""),
|
|
140
|
+
repositories: text("repositories").array().notNull().default([]),
|
|
139
141
|
nextNumber: integer("next_number").notNull().default(1),
|
|
140
142
|
position: bigint("position", { mode: "number" }).notNull().default(0),
|
|
141
143
|
version: integer("version").notNull().default(1),
|
|
@@ -304,6 +306,7 @@ const receipts = pgTable("mutation_receipts", {
|
|
|
304
306
|
operation: text("operation").notNull(),
|
|
305
307
|
hash: text("hash").notNull(),
|
|
306
308
|
result: jsonb("result").notNull(),
|
|
309
|
+
projectId: uuid("project_id"),
|
|
307
310
|
createdAt: time("created_at")
|
|
308
311
|
}, (table) => [primaryKey({ columns: [table.actorId, table.requestId] })]);
|
|
309
312
|
const storageBackends = pgTable("storage_backends", {
|
|
@@ -369,6 +372,20 @@ const stagedUploads = pgTable("staged_uploads", {
|
|
|
369
372
|
size: integer("size").notNull(),
|
|
370
373
|
createdAt: time("created_at")
|
|
371
374
|
}, (table) => [index("staged_uploads_created").on(table.createdAt)]);
|
|
375
|
+
const uploadUrls = pgTable("upload_urls", {
|
|
376
|
+
id: uuid("id").primaryKey(),
|
|
377
|
+
projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
378
|
+
ticketId: uuid("ticket_id").references(() => tickets.id, { onDelete: "cascade" }),
|
|
379
|
+
actorId: text("actor_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
380
|
+
credential: jsonb("credential"),
|
|
381
|
+
name: text("name").notNull(),
|
|
382
|
+
contentType: text("content_type").notNull(),
|
|
383
|
+
size: integer("size").notNull(),
|
|
384
|
+
tokenHash: text("token_hash").notNull().unique(),
|
|
385
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
386
|
+
usedAt: timestamp("used_at", { withTimezone: true }),
|
|
387
|
+
createdAt: time("created_at")
|
|
388
|
+
}, (table) => [index("upload_urls_expires").on(table.expiresAt)]);
|
|
372
389
|
const bytea = customType({ dataType: () => "bytea" });
|
|
373
390
|
const attachmentContent = pgTable("attachment_content", {
|
|
374
391
|
id: uuid("id").primaryKey().references(() => attachments.id, { onDelete: "cascade" }),
|
|
@@ -839,6 +856,7 @@ function createBlobStorage(database, instance, factory = createObjectStore, sche
|
|
|
839
856
|
});
|
|
840
857
|
return database.db.transaction(async (tx) => {
|
|
841
858
|
await tx.delete(stagedUploads).where(lt(stagedUploads.createdAt, stagedUploadCutoff()));
|
|
859
|
+
await tx.delete(uploadUrls).where(lt(uploadUrls.expiresAt, /* @__PURE__ */ new Date()));
|
|
842
860
|
const rows = await tx.select({ object: blobObjects }).from(blobObjects).leftJoin(attachments, eq(attachments.objectId, blobObjects.id)).leftJoin(profilePhotos, eq(profilePhotos.objectId, blobObjects.id)).leftJoin(stagedUploads, eq(stagedUploads.id, blobObjects.id)).leftJoin(projects, or(eq(projects.iconObjectId, blobObjects.id), eq(projects.backgroundObjectId, blobObjects.id))).where(and(isNull(attachments.id), isNull(profilePhotos.userId), isNull(stagedUploads.id), isNull(projects.id), lt(blobObjects.createdAt, /* @__PURE__ */ new Date(Date.now() - 864e5)))).limit(20).for("update", {
|
|
843
861
|
of: blobObjects,
|
|
844
862
|
skipLocked: true
|
|
@@ -1091,14 +1109,23 @@ function parseQuery(query) {
|
|
|
1091
1109
|
terms: query
|
|
1092
1110
|
};
|
|
1093
1111
|
}
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1112
|
+
const phrasePattern = (phrase, word) => `(?<!${word})${phrase.replace(/[\\^$.|?*+()[\]{}]/gu, "\\$&").replaceAll(" ", "\\s+")}(?!${word})`;
|
|
1113
|
+
const phraseRegExp = (phrase) => new RegExp(phrasePattern(phrase, "[\\p{L}\\p{N}_]"), "iu");
|
|
1114
|
+
const segmentTests = ({ phrases, terms }) => [...phrases.map((phrase) => {
|
|
1115
|
+
const pattern = phrasePattern(phrase, "[[:alnum:]_]");
|
|
1116
|
+
return (text) => sql`${text} ~* ${pattern}`;
|
|
1117
|
+
}), ...terms ? [(text) => sql`${text} ILIKE ${likePattern(terms)}`] : []];
|
|
1118
|
+
const searchedText = [
|
|
1119
|
+
tickets.title,
|
|
1120
|
+
tickets.description,
|
|
1121
|
+
tickets.key,
|
|
1122
|
+
tickets.acceptanceCriteria,
|
|
1123
|
+
sql`array_to_string(${tickets.labels}, ' ')`,
|
|
1124
|
+
sql`array_to_string(${tickets.scopes}, ' ')`
|
|
1125
|
+
];
|
|
1126
|
+
const textMatch = (query) => and(...segmentTests(query).map((test) => or(...searchedText.map(test))));
|
|
1127
|
+
const postgresSearch = (query) => textMatch(parseQuery(query));
|
|
1128
|
+
const postgresRank = (query) => sql`CASE WHEN ${and(...segmentTests(parseQuery(query)).map((test) => test(tickets.title)))} THEN 0 ELSE 1 END`;
|
|
1102
1129
|
const builtinSearch = { where: async (_db, _ids, query) => ({
|
|
1103
1130
|
where: postgresSearch(query),
|
|
1104
1131
|
rank: postgresRank(query),
|
|
@@ -1164,13 +1191,17 @@ function createTicketSearch(database, instance, factory = createTicketIndex, sch
|
|
|
1164
1191
|
if (!state || !clock || state.error || state.configHash !== fingerprint(settings) || state.cursor !== clock.value) return fallback();
|
|
1165
1192
|
const root = await instance.read();
|
|
1166
1193
|
try {
|
|
1194
|
+
const parsed = parseQuery(query);
|
|
1167
1195
|
const hits = await factory({
|
|
1168
1196
|
provider: "elasticsearch",
|
|
1169
1197
|
config: settings
|
|
1170
|
-
}).find(root.identity, projectIds,
|
|
1198
|
+
}).find(root.identity, projectIds, parsed);
|
|
1171
1199
|
const ids = hits.map((hit) => hit.id), ranks = hits.map((hit) => hit.rank);
|
|
1172
1200
|
return {
|
|
1173
|
-
where: ids.length ? inArray(tickets.id, ids)
|
|
1201
|
+
where: ids.length ? and(inArray(tickets.id, ids), textMatch({
|
|
1202
|
+
phrases: parsed.phrases,
|
|
1203
|
+
terms: ""
|
|
1204
|
+
})) : sql`false`,
|
|
1174
1205
|
rank: sql`(${`{${ranks.join(",")}}`}::int[])[array_position(${`{${ids.join(",")}}`}::uuid[], ${tickets.id})]`,
|
|
1175
1206
|
engine: "index",
|
|
1176
1207
|
indexing: false
|
|
@@ -2413,14 +2444,19 @@ function predicateSQL({ field, operator, values }) {
|
|
|
2413
2444
|
const overlap = sql`${tickets.labels} && ${sql.param(values)}::text[]`;
|
|
2414
2445
|
return operator === "any" ? overlap : sql`NOT (${overlap})`;
|
|
2415
2446
|
}
|
|
2447
|
+
if (field === "automation") {
|
|
2448
|
+
const ready = sql`(${tickets.automationReady} AND ${tickets.holdReason} IS NULL)`;
|
|
2449
|
+
const match = or(...values.map((value) => value === "ready" ? ready : sql`NOT ${ready}`));
|
|
2450
|
+
return operator === "any" ? match : sql`NOT (${match})`;
|
|
2451
|
+
}
|
|
2416
2452
|
const column = field === "assigneeId" ? tickets.assigneeId : filterColumns[field];
|
|
2417
2453
|
const named = field === "assigneeId" ? values.filter((value) => value !== "none") : values, unset = field === "assigneeId" && values.includes("none");
|
|
2418
2454
|
if (operator === "any") return or(named.length ? inArray(column, named) : void 0, unset ? isNull(column) : void 0);
|
|
2419
2455
|
return and(named.length ? or(isNull(column), notInArray(column, named)) : void 0, unset ? isNotNull(column) : void 0);
|
|
2420
2456
|
}
|
|
2421
2457
|
const predicateWhere = (filter) => and(...ticketPredicates(filter).map(predicateSQL));
|
|
2422
|
-
function ticketOrder(
|
|
2423
|
-
const direction =
|
|
2458
|
+
function ticketOrder({ sort = "priority", order }) {
|
|
2459
|
+
const direction = order ?? defaultOrder[sort];
|
|
2424
2460
|
const by = (column) => direction === "asc" ? sql`${column} ASC NULLS LAST` : sql`${column} DESC NULLS LAST`;
|
|
2425
2461
|
return [{
|
|
2426
2462
|
priority: by(rank(tickets.priority, priorities)),
|
|
@@ -2436,7 +2472,28 @@ function ticketOrder(input) {
|
|
|
2436
2472
|
kind: by(tickets.kind),
|
|
2437
2473
|
assignee: by(sql`(SELECT lower(name) FROM "user" WHERE id = ${tickets.assigneeId})`),
|
|
2438
2474
|
manual: by(tickets.position)
|
|
2439
|
-
}[
|
|
2475
|
+
}[sort], asc(tickets.number)];
|
|
2476
|
+
}
|
|
2477
|
+
const relevanceOrder = (q, relevance) => [
|
|
2478
|
+
sql`CASE WHEN ${ticketMatch(q)} THEN 0 ELSE 1 END`,
|
|
2479
|
+
sql`${relevance} NULLS LAST`,
|
|
2480
|
+
desc(tickets.updatedAt)
|
|
2481
|
+
];
|
|
2482
|
+
async function listQuery(ctx, projectIds, input) {
|
|
2483
|
+
if (!input.q) return {
|
|
2484
|
+
where: void 0,
|
|
2485
|
+
order: ticketOrder(input)
|
|
2486
|
+
};
|
|
2487
|
+
const where = or(ticketMatch(input.q), postgresSearch(input.q));
|
|
2488
|
+
if (input.sort) return {
|
|
2489
|
+
where,
|
|
2490
|
+
order: ticketOrder(input)
|
|
2491
|
+
};
|
|
2492
|
+
const match = await ctx.search.where(ctx.db, projectIds, input.q);
|
|
2493
|
+
return {
|
|
2494
|
+
where,
|
|
2495
|
+
order: [...relevanceOrder(input.q, match.rank), asc(tickets.number)]
|
|
2496
|
+
};
|
|
2440
2497
|
}
|
|
2441
2498
|
async function pageOf(ctx, where, order, input) {
|
|
2442
2499
|
const rows = await ctx.db.select({
|
|
@@ -2527,12 +2584,16 @@ const hitRow = z.object({
|
|
|
2527
2584
|
const summarize = (ticket) => ticketSummarySchema.parse(json(ticket));
|
|
2528
2585
|
const snippetLength = 240;
|
|
2529
2586
|
function snippet(body, query) {
|
|
2530
|
-
const text = body?.replace(/\s+/gu, " ").trim() ?? "", lower = text.toLowerCase(),
|
|
2587
|
+
const text = body?.replace(/\s+/gu, " ").trim() ?? "", lower = text.toLowerCase(), { phrases, terms } = parseQuery(query);
|
|
2531
2588
|
let at = -1, size = 0;
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2589
|
+
const closest = (found, length) => {
|
|
2590
|
+
if (found >= 0 && (at < 0 || found < at)) [at, size] = [found, length];
|
|
2591
|
+
};
|
|
2592
|
+
for (const phrase of phrases) {
|
|
2593
|
+
const found = phraseRegExp(phrase).exec(text);
|
|
2594
|
+
if (found) closest(found.index, found[0].length);
|
|
2535
2595
|
}
|
|
2596
|
+
for (const needle of [terms, ...terms.split(" ")]) if (needle) closest(lower.indexOf(needle.toLowerCase()), needle.length);
|
|
2536
2597
|
if (at < 0) return "";
|
|
2537
2598
|
if (text.length <= snippetLength) return text;
|
|
2538
2599
|
const start = Math.max(0, Math.min(at + Math.floor((size - snippetLength) / 2), text.length - snippetLength)), end = start + snippetLength;
|
|
@@ -2664,9 +2725,9 @@ const ticketHandlers = {
|
|
|
2664
2725
|
async list_tickets(ctx, input) {
|
|
2665
2726
|
const parent = input.parentId ? await getTicket(ctx, input.parentId) : void 0;
|
|
2666
2727
|
const related = input.relatedTo ? await getTicket(ctx, input.relatedTo) : void 0;
|
|
2667
|
-
const search =
|
|
2728
|
+
const search = await listQuery(ctx, [project(ctx).id], input);
|
|
2668
2729
|
await assertCustomFieldFilters(ctx, [project(ctx).id], input);
|
|
2669
|
-
const page = await pageOf(ctx, and(eq(tickets.projectId, project(ctx).id), parent ? eq(tickets.parentId, parent.id) : void 0, related ? or(inArray(tickets.id, ctx.db.select({ id: relations$1.targetId }).from(relations$1).where(eq(relations$1.sourceId, related.id))), inArray(tickets.id, ctx.db.select({ id: relations$1.sourceId }).from(relations$1).where(eq(relations$1.targetId, related.id)))) : void 0, predicateWhere(input), input.archived === "include" ? void 0 : eq(tickets.archived, input.archived === "only"), dueWhere(input.due), input.stateId ? eq(tickets.stateId, input.stateId) : void 0, input.scheduledFrom ? sql`${scheduleEnd} >= ${input.scheduledFrom}::timestamptz` : void 0, input.scheduledTo ? sql`${scheduleStart} < ${input.scheduledTo}::timestamptz` : void 0, search
|
|
2730
|
+
const page = await pageOf(ctx, and(eq(tickets.projectId, project(ctx).id), parent ? eq(tickets.parentId, parent.id) : void 0, related ? or(inArray(tickets.id, ctx.db.select({ id: relations$1.targetId }).from(relations$1).where(eq(relations$1.sourceId, related.id))), inArray(tickets.id, ctx.db.select({ id: relations$1.sourceId }).from(relations$1).where(eq(relations$1.targetId, related.id)))) : void 0, predicateWhere(input), input.archived === "include" ? void 0 : eq(tickets.archived, input.archived === "only"), dueWhere(input.due), input.stateId ? eq(tickets.stateId, input.stateId) : void 0, input.scheduledFrom ? sql`${scheduleEnd} >= ${input.scheduledFrom}::timestamptz` : void 0, input.scheduledTo ? sql`${scheduleStart} < ${input.scheduledTo}::timestamptz` : void 0, search.where), search.order, input);
|
|
2670
2731
|
return input.view === "summary" ? {
|
|
2671
2732
|
...page,
|
|
2672
2733
|
items: page.items.map(summarize)
|
|
@@ -2679,11 +2740,11 @@ const ticketHandlers = {
|
|
|
2679
2740
|
total: 0,
|
|
2680
2741
|
activity: []
|
|
2681
2742
|
};
|
|
2682
|
-
const ids = allowed.map((row) => row.id);
|
|
2683
|
-
const where = and(inArray(tickets.projectId, ids), eq(tickets.assigneeId, ctx.actor.id), eq(tickets.archived, false), openTicket,
|
|
2743
|
+
const ids = allowed.map((row) => row.id), search = await listQuery(ctx, ids, input);
|
|
2744
|
+
const where = and(inArray(tickets.projectId, ids), eq(tickets.assigneeId, ctx.actor.id), eq(tickets.archived, false), openTicket, search.where);
|
|
2684
2745
|
const activity = input.activity ? await ctx.db.select().from(events).where(inArray(events.projectId, ids)).orderBy(desc(events.id)).limit(input.activity) : [];
|
|
2685
2746
|
return {
|
|
2686
|
-
...await pageOf(ctx, where,
|
|
2747
|
+
...await pageOf(ctx, where, search.order, input),
|
|
2687
2748
|
activity
|
|
2688
2749
|
};
|
|
2689
2750
|
},
|
|
@@ -2898,7 +2959,7 @@ const ticketHandlers = {
|
|
|
2898
2959
|
const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
|
|
2899
2960
|
if (!allowed.length) return { items: [] };
|
|
2900
2961
|
const ids = allowed.map((row) => row.id), match = await ctx.search.where(ctx.db, ids, input.q);
|
|
2901
|
-
return { items: (await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, ids), eq(tickets.archived, false), or(ticketMatch(input.q), match.where))).orderBy(
|
|
2962
|
+
return { items: (await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, ids), eq(tickets.archived, false), or(ticketMatch(input.q), match.where))).orderBy(...relevanceOrder(input.q, match.rank)).limit(input.limit)).flatMap((ticket) => {
|
|
2902
2963
|
const owner = allowed.find((row) => row.id === ticket.projectId);
|
|
2903
2964
|
if (!owner) return [];
|
|
2904
2965
|
const summary = input.view === "summary", body = `${ticket.description} ${ticket.acceptanceCriteria}`;
|
|
@@ -3018,7 +3079,7 @@ const ticketHandlers = {
|
|
|
3018
3079
|
},
|
|
3019
3080
|
async claim_ticket(ctx, input) {
|
|
3020
3081
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
3021
|
-
assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
|
|
3082
|
+
if (input.expectedVersion !== void 0) assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
|
|
3022
3083
|
const eligible = await readiness(ctx, ticket);
|
|
3023
3084
|
if (!eligible.ready) fail(409, "not_ready", "The ticket is not ready to claim", eligible);
|
|
3024
3085
|
const claim = one(await ctx.db.insert(claims).values({
|
|
@@ -3040,7 +3101,10 @@ const ticketHandlers = {
|
|
|
3040
3101
|
workerId: claim.workerId,
|
|
3041
3102
|
version: row.version
|
|
3042
3103
|
});
|
|
3043
|
-
return
|
|
3104
|
+
return {
|
|
3105
|
+
...claim,
|
|
3106
|
+
ticket: row
|
|
3107
|
+
};
|
|
3044
3108
|
},
|
|
3045
3109
|
async renew_claim(ctx, input) {
|
|
3046
3110
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
@@ -3411,6 +3475,90 @@ async function addComment(ctx, input) {
|
|
|
3411
3475
|
});
|
|
3412
3476
|
return row;
|
|
3413
3477
|
}
|
|
3478
|
+
async function attachFile(ctx, ticket, id, bytes, file) {
|
|
3479
|
+
if (intakeOnly(ctx.actor)) {
|
|
3480
|
+
const stored = one(await ctx.db.select({
|
|
3481
|
+
files: sql`count(*) filter (where ${attachments.ticketId} = ${ticket.id})`.mapWith(Number),
|
|
3482
|
+
bytes: sql`coalesce(sum(${attachments.size}), 0)`.mapWith(Number)
|
|
3483
|
+
}).from(attachments).innerJoin(tickets, eq(tickets.id, attachments.ticketId)).where(and(eq(tickets.intakeCredentialId, ctx.actor.ownerId), eq(tickets.holdReason, intakeHoldReason))));
|
|
3484
|
+
if (stored.files >= 10) fail(409, "attachment_limit", `An intake credential can attach at most 10 files to a ticket`);
|
|
3485
|
+
if (stored.bytes + bytes.length > 1e9) fail(409, "attachment_limit", "This intake credential's untriaged tickets hold too many attachment bytes");
|
|
3486
|
+
}
|
|
3487
|
+
await ctx.blobs.put(ctx.db, id, bytes, file.contentType);
|
|
3488
|
+
const row = one(await ctx.db.insert(attachments).values({
|
|
3489
|
+
id,
|
|
3490
|
+
objectId: id,
|
|
3491
|
+
projectId: project(ctx).id,
|
|
3492
|
+
ticketId: ticket.id,
|
|
3493
|
+
actorId: ctx.actor.id,
|
|
3494
|
+
name: file.name,
|
|
3495
|
+
contentType: file.contentType,
|
|
3496
|
+
size: bytes.length
|
|
3497
|
+
}).returning());
|
|
3498
|
+
await publish(ctx, "attachment.created", ticket.id, {
|
|
3499
|
+
attachmentId: row.id,
|
|
3500
|
+
size: row.size
|
|
3501
|
+
});
|
|
3502
|
+
return row;
|
|
3503
|
+
}
|
|
3504
|
+
async function stageFile(ctx, id, bytes, file) {
|
|
3505
|
+
await ctx.blobs.put(ctx.db, id, bytes, file.contentType);
|
|
3506
|
+
const row = one(await ctx.db.insert(stagedUploads).values({
|
|
3507
|
+
id,
|
|
3508
|
+
projectId: project(ctx).id,
|
|
3509
|
+
actorId: ctx.actor.id,
|
|
3510
|
+
name: file.name,
|
|
3511
|
+
contentType: file.contentType,
|
|
3512
|
+
size: bytes.length
|
|
3513
|
+
}).returning());
|
|
3514
|
+
return {
|
|
3515
|
+
...row,
|
|
3516
|
+
expiresAt: new Date(row.createdAt.getTime() + stagedUploadLifetime)
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
const uploadToken = /^kvup_[A-Za-z0-9_-]{43}$/;
|
|
3520
|
+
const hashUploadToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
3521
|
+
const uploadCredential = z.object({
|
|
3522
|
+
ownerId: z.string(),
|
|
3523
|
+
scopes: z.array(z.enum(scopes)),
|
|
3524
|
+
projectIds: z.array(z.string()).optional(),
|
|
3525
|
+
apiKey: z.boolean()
|
|
3526
|
+
}).nullable();
|
|
3527
|
+
function assertUploadUrlOpen(row) {
|
|
3528
|
+
if (row.usedAt) fail(410, "upload_url_unavailable", "This upload URL was already used");
|
|
3529
|
+
if (row.expiresAt.getTime() <= Date.now()) fail(410, "upload_url_unavailable", "This upload URL expired; request a new one");
|
|
3530
|
+
}
|
|
3531
|
+
async function receiveUpload(domain, token, contentLength, read) {
|
|
3532
|
+
const { db } = domain.database, row = uploadToken.test(token) ? (await db.select().from(uploadUrls).where(eq(uploadUrls.tokenHash, hashUploadToken(token))))[0] : void 0;
|
|
3533
|
+
if (!row) return fail(404, "not_found", "This upload URL does not exist");
|
|
3534
|
+
assertUploadUrlOpen(row);
|
|
3535
|
+
const wrongSize = (sent) => fail(400, "invalid_attachment", `This upload URL takes exactly ${row.size} bytes with that Content-Length; the request sent ${sent}`);
|
|
3536
|
+
if (contentLength !== String(row.size)) wrongSize(contentLength ?? "no Content-Length");
|
|
3537
|
+
const reserved = /* @__PURE__ */ new Date();
|
|
3538
|
+
const [held] = await db.update(uploadUrls).set({ usedAt: reserved }).where(and(eq(uploadUrls.id, row.id), isNull(uploadUrls.usedAt), gt(uploadUrls.expiresAt, reserved))).returning({ id: uploadUrls.id });
|
|
3539
|
+
if (!held) fail(410, "upload_url_unavailable", "This upload URL is already receiving an upload");
|
|
3540
|
+
try {
|
|
3541
|
+
const bytes = await read(row.size);
|
|
3542
|
+
if (bytes.length !== row.size) wrongSize(bytes.length > row.size ? "more" : String(bytes.length));
|
|
3543
|
+
const credential = uploadCredential.parse(row.credential);
|
|
3544
|
+
if (credential?.apiKey) {
|
|
3545
|
+
const key = (await db.select().from(apikey).where(eq(apikey.id, credential.ownerId)))[0];
|
|
3546
|
+
if (!key?.enabled || key.expiresAt && key.expiresAt <= /* @__PURE__ */ new Date()) fail(401, "unauthorized", "The API credential that requested this upload URL was revoked");
|
|
3547
|
+
}
|
|
3548
|
+
const actor = await domain.actor(row.actorId, credential ? {
|
|
3549
|
+
ownerId: credential.ownerId,
|
|
3550
|
+
scopes: credential.scopes,
|
|
3551
|
+
...credential.projectIds ? { projectIds: credential.projectIds } : {}
|
|
3552
|
+
} : void 0);
|
|
3553
|
+
return await domain.transact(async (tx, as) => {
|
|
3554
|
+
const ctx = await as(actor, row.projectId);
|
|
3555
|
+
return row.ticketId ? attachmentSchema.parse(json(await attachFile(ctx, await getTicket(ctx, row.ticketId), row.id, bytes, row))) : stagedUploadSchema.parse(json(await stageFile(ctx, row.id, bytes, row)));
|
|
3556
|
+
});
|
|
3557
|
+
} catch (error) {
|
|
3558
|
+
await db.update(uploadUrls).set({ usedAt: null }).where(and(eq(uploadUrls.id, row.id), eq(uploadUrls.usedAt, reserved)));
|
|
3559
|
+
throw error;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3414
3562
|
const collaborationHandlers = {
|
|
3415
3563
|
async list_relations(ctx, input) {
|
|
3416
3564
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
@@ -3520,49 +3668,34 @@ const collaborationHandlers = {
|
|
|
3520
3668
|
};
|
|
3521
3669
|
},
|
|
3522
3670
|
async upload_attachment(ctx, input) {
|
|
3523
|
-
|
|
3524
|
-
const bytes = decodeBlobData(input.data, attachmentLimit);
|
|
3525
|
-
if (intakeOnly(ctx.actor)) {
|
|
3526
|
-
const stored = one(await ctx.db.select({
|
|
3527
|
-
files: sql`count(*) filter (where ${attachments.ticketId} = ${ticket.id})`.mapWith(Number),
|
|
3528
|
-
bytes: sql`coalesce(sum(${attachments.size}), 0)`.mapWith(Number)
|
|
3529
|
-
}).from(attachments).innerJoin(tickets, eq(tickets.id, attachments.ticketId)).where(and(eq(tickets.intakeCredentialId, ctx.actor.ownerId), eq(tickets.holdReason, intakeHoldReason))));
|
|
3530
|
-
if (stored.files >= 10) fail(409, "attachment_limit", `An intake credential can attach at most 10 files to a ticket`);
|
|
3531
|
-
if (stored.bytes + bytes.length > 1e9) fail(409, "attachment_limit", "This intake credential's untriaged tickets hold too many attachment bytes");
|
|
3532
|
-
}
|
|
3533
|
-
const id = crypto.randomUUID();
|
|
3534
|
-
await ctx.blobs.put(ctx.db, id, bytes, input.contentType);
|
|
3535
|
-
const row = one(await ctx.db.insert(attachments).values({
|
|
3536
|
-
id,
|
|
3537
|
-
objectId: id,
|
|
3538
|
-
projectId: project(ctx).id,
|
|
3539
|
-
ticketId: ticket.id,
|
|
3540
|
-
actorId: ctx.actor.id,
|
|
3541
|
-
name: input.name,
|
|
3542
|
-
contentType: input.contentType,
|
|
3543
|
-
size: bytes.length
|
|
3544
|
-
}).returning());
|
|
3545
|
-
await publish(ctx, "attachment.created", ticket.id, {
|
|
3546
|
-
attachmentId: row.id,
|
|
3547
|
-
size: row.size
|
|
3548
|
-
});
|
|
3549
|
-
return row;
|
|
3671
|
+
return attachFile(ctx, await getTicket(ctx, input.ticketId), crypto.randomUUID(), decodeBlobData(input.data, attachmentLimit), input);
|
|
3550
3672
|
},
|
|
3551
3673
|
async stage_upload(ctx, input) {
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3674
|
+
return stageFile(ctx, crypto.randomUUID(), decodeBlobData(input.data, attachmentLimit), input);
|
|
3675
|
+
},
|
|
3676
|
+
async create_upload_url(ctx, input) {
|
|
3677
|
+
const ticket = input.ticketId ? await getTicket(ctx, input.ticketId) : void 0, token = `kvup_${randomBytes(32).toString("base64url")}`, { id, ownerId, scopes: granted, projectIds, machine } = ctx.actor, apiKey = machine && (await ctx.db.select({ id: apikey.id }).from(apikey).where(eq(apikey.id, ownerId))).length > 0;
|
|
3678
|
+
const row = one(await ctx.db.insert(uploadUrls).values({
|
|
3679
|
+
id: crypto.randomUUID(),
|
|
3557
3680
|
projectId: project(ctx).id,
|
|
3558
|
-
|
|
3681
|
+
ticketId: ticket?.id ?? null,
|
|
3682
|
+
actorId: id,
|
|
3683
|
+
credential: machine ? {
|
|
3684
|
+
ownerId,
|
|
3685
|
+
scopes: granted,
|
|
3686
|
+
projectIds,
|
|
3687
|
+
apiKey
|
|
3688
|
+
} : null,
|
|
3559
3689
|
name: input.name,
|
|
3560
3690
|
contentType: input.contentType,
|
|
3561
|
-
size:
|
|
3691
|
+
size: input.size,
|
|
3692
|
+
tokenHash: hashUploadToken(token),
|
|
3693
|
+
expiresAt: new Date(Date.now() + uploadUrlLifetime)
|
|
3562
3694
|
}).returning());
|
|
3563
3695
|
return {
|
|
3564
|
-
|
|
3565
|
-
|
|
3696
|
+
uploadId: row.id,
|
|
3697
|
+
url: new URL(`/api/uploads/${token}`, ctx.publicUrl).href,
|
|
3698
|
+
expiresAt: row.expiresAt
|
|
3566
3699
|
};
|
|
3567
3700
|
},
|
|
3568
3701
|
async discard_upload(ctx, input) {
|
|
@@ -5326,6 +5459,11 @@ async function takePrefix(ctx, prefix, projectId) {
|
|
|
5326
5459
|
projectId: owner
|
|
5327
5460
|
}).onConflictDoNothing();
|
|
5328
5461
|
}
|
|
5462
|
+
async function assertRepositoriesFree(ctx, projectId, workspaceId, repositories) {
|
|
5463
|
+
if (!repositories.length) return;
|
|
5464
|
+
const [holder] = await ctx.db.select({ id: projects.id }).from(projects).where(and(eq(projects.workspaceId, workspaceId), isNull(projects.deletedAt), isNull(projects.archivedAt), ne(projects.id, projectId), arrayOverlaps(projects.repositories, repositories))).limit(1);
|
|
5465
|
+
if (holder) fail(409, "duplicate_repository", "Another project in this workspace links this repository");
|
|
5466
|
+
}
|
|
5329
5467
|
async function storeImage(ctx, image) {
|
|
5330
5468
|
const objectId = crypto.randomUUID();
|
|
5331
5469
|
await ctx.blobs.put(ctx.db, objectId, decodeImage(image), image.contentType);
|
|
@@ -5441,18 +5579,20 @@ const projectHandlers = {
|
|
|
5441
5579
|
const current = project(ctx);
|
|
5442
5580
|
assertVersion(current, input.expectedVersion, "project");
|
|
5443
5581
|
const key = input.key ?? current.key;
|
|
5444
|
-
const sharing = input.publicReadEnabled !== void 0 && input.publicReadEnabled !== current.publicReadEnabled, details = key !== current.key || input.name !== current.name || input.description !== current.description || !!input.visibility && input.visibility !== current.visibility;
|
|
5582
|
+
const sharing = input.publicReadEnabled !== void 0 && input.publicReadEnabled !== current.publicReadEnabled, details = key !== current.key || input.name !== current.name || input.description !== current.description || !!input.visibility && input.visibility !== current.visibility || !!input.repositories && !isDeepStrictEqual(input.repositories, current.repositories);
|
|
5445
5583
|
if (sharing) requirePermission(ctx.permissions, "sharing.manage", "project");
|
|
5446
5584
|
if (details || !sharing) requirePermission(ctx.permissions, "project.manage", "project");
|
|
5447
5585
|
if (key !== current.key) {
|
|
5448
5586
|
await (await takePrefix(ctx, key, current.id))(current.id);
|
|
5449
5587
|
await ctx.db.update(tickets).set({ key: sql`${key}::text || '-' || ${tickets.number}` }).where(eq(tickets.projectId, current.id));
|
|
5450
5588
|
}
|
|
5589
|
+
if (input.repositories && !current.archivedAt) await assertRepositoriesFree(ctx, current.id, current.workspaceId, input.repositories);
|
|
5451
5590
|
const row = one(await ctx.db.update(projects).set({
|
|
5452
5591
|
key,
|
|
5453
5592
|
name: input.name,
|
|
5454
5593
|
...input.visibility ? { visibility: input.visibility } : {},
|
|
5455
5594
|
...input.publicReadEnabled === void 0 ? {} : { publicReadEnabled: input.publicReadEnabled },
|
|
5595
|
+
...input.repositories ? { repositories: input.repositories } : {},
|
|
5456
5596
|
description: input.description,
|
|
5457
5597
|
version: current.version + 1,
|
|
5458
5598
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -5870,6 +6010,7 @@ async function assertTransferable(ctx, workspaceId) {
|
|
|
5870
6010
|
const current = project(ctx);
|
|
5871
6011
|
if (workspaceId === current.workspaceId) fail(400, "validation_error", "The project is already in this workspace");
|
|
5872
6012
|
await requireWorkspace(ctx, current.workspaceId, "project.manage");
|
|
6013
|
+
if (!current.archivedAt) await assertRepositoriesFree(ctx, current.id, workspaceId, current.repositories);
|
|
5873
6014
|
}
|
|
5874
6015
|
async function projectPeople(ctx, workspaceId = project(ctx).workspaceId) {
|
|
5875
6016
|
const current = project(ctx), qualifiedUser = sql`${user}."id"`;
|
|
@@ -6032,6 +6173,7 @@ const lifecycleHandlers = {
|
|
|
6032
6173
|
assertVersion(current, input.expectedVersion, "project", input.archived ? "archiving" : "restoring");
|
|
6033
6174
|
if (!!current.archivedAt === input.archived) return current;
|
|
6034
6175
|
if (input.archived) assertNoBlockers(await claimBlockers(ctx));
|
|
6176
|
+
else await assertRepositoriesFree(ctx, current.id, current.workspaceId, current.repositories);
|
|
6035
6177
|
const row = await changed(ctx, { archivedAt: input.archived ? /* @__PURE__ */ new Date() : null });
|
|
6036
6178
|
await publish(ctx, input.archived ? "project.archived" : "project.restored", null, { version: row.version });
|
|
6037
6179
|
return row;
|
|
@@ -7058,6 +7200,16 @@ const publicActor = {
|
|
|
7058
7200
|
projectIds: []
|
|
7059
7201
|
};
|
|
7060
7202
|
const projectMatch = (identifier) => or(sql`${projects.id}::text = ${identifier}`, eq(projects.key, identifier.toUpperCase()), sql`${projects.id} IN (SELECT pp.project_id FROM ${projectPrefixes} pp WHERE pp.prefix = ${identifier.toUpperCase()})`);
|
|
7203
|
+
const namesRemote = (identifier) => /[/@]/.test(identifier);
|
|
7204
|
+
async function projectByRemote(db, actor, identifier) {
|
|
7205
|
+
const remote = normalizeRemote(identifier);
|
|
7206
|
+
const linked = remote ? await db.select().from(projects).where(and(isNull(projects.deletedAt), arrayContains(projects.repositories, [remote]))) : [];
|
|
7207
|
+
const reachable = [];
|
|
7208
|
+
for (const row of linked) if ((!actor.projectIds || actor.projectIds.includes(row.id)) && await projectAccess(db, actor, row)) reachable.push(row);
|
|
7209
|
+
const live = reachable.filter((row) => !row.archivedAt), named = live.length ? live : reachable;
|
|
7210
|
+
if (named.length > 1) fail(409, "ambiguous_project", "More than one of your projects links this repository", { projects: named.map((row) => row.key) });
|
|
7211
|
+
return one(named);
|
|
7212
|
+
}
|
|
7061
7213
|
const issuedKey = /^([A-Z][A-Z0-9]{1,9})-([1-9]\d{0,8})$/;
|
|
7062
7214
|
function ticketMatch(identifier) {
|
|
7063
7215
|
const key = identifier.toUpperCase(), [, prefix, number] = issuedKey.exec(key) ?? [];
|
|
@@ -7073,7 +7225,7 @@ async function context(db, actor, scope, projectId, workspaceId, permission = sc
|
|
|
7073
7225
|
ctx.permissions = access.permissions;
|
|
7074
7226
|
}
|
|
7075
7227
|
if (!projectId) return ctx;
|
|
7076
|
-
const row = one(await db.select().from(projects).where(and(isNull(projects.deletedAt), projectMatch(projectId))).limit(1));
|
|
7228
|
+
const row = namesRemote(projectId) ? await projectByRemote(db, actor, projectId) : one(await db.select().from(projects).where(and(isNull(projects.deletedAt), projectMatch(projectId))).limit(1));
|
|
7077
7229
|
if (actor.projectIds && !actor.projectIds.includes(row.id)) fail(404, "not_found", "Project was not found");
|
|
7078
7230
|
const access = await projectAccess(db, actor, row);
|
|
7079
7231
|
if (!access) fail(404, "not_found", "Project was not found");
|
|
@@ -7380,10 +7532,13 @@ function createDomain(database, services = {}) {
|
|
|
7380
7532
|
const receipt = requestId ? (await tx.select().from(receipts).where(and(eq(receipts.actorId, actor.id), eq(receipts.requestId, requestId))).limit(1))[0] : void 0;
|
|
7381
7533
|
if (receipt && (receipt.hash !== hash || receipt.operation !== name)) fail(409, "idempotency_conflict", "This requestId was already used for another write");
|
|
7382
7534
|
if (receipt && (name === "delete_project" || name === "delete_workspace")) return parseOutput(name, receipt.result);
|
|
7383
|
-
const ctx = await context(tx, actor, spec.scope, projectId, workspaceId, permission, name);
|
|
7535
|
+
const ctx = await context(tx, actor, spec.scope, receipt?.projectId ?? projectId, workspaceId, permission, name);
|
|
7384
7536
|
if (receipt) {
|
|
7385
7537
|
if ((name === "create_time_entry" || name === "update_time_entry" || name === "delete_time_entry") && "ticketId" in input) await getTicket(ctx, input.ticketId);
|
|
7386
|
-
const prior = parseOutput(name, receipt.result)
|
|
7538
|
+
const prior = parseOutput(name, name === "claim_ticket" && "ticketId" in input && !operations.claim_ticket.output.safeParse(receipt.result).success ? json({
|
|
7539
|
+
...claimSchema.parse(receipt.result),
|
|
7540
|
+
ticket: await getTicket(ctx, input.ticketId)
|
|
7541
|
+
}) : receipt.result);
|
|
7387
7542
|
if (name === "create_project") await context(tx, actor, "read", projectSchema.parse(prior).id);
|
|
7388
7543
|
if (name === "create_workspace") await requireWorkspace(ctx, workspaceSchema.parse(prior).id, null);
|
|
7389
7544
|
if (name === "relocate_ticket") await context(tx, actor, "write", ticketSchema.parse(prior).projectId);
|
|
@@ -7396,7 +7551,8 @@ function createDomain(database, services = {}) {
|
|
|
7396
7551
|
requestId,
|
|
7397
7552
|
hash,
|
|
7398
7553
|
operation: name,
|
|
7399
|
-
result
|
|
7554
|
+
result,
|
|
7555
|
+
projectId: ctx.project?.id ?? null
|
|
7400
7556
|
});
|
|
7401
7557
|
return result;
|
|
7402
7558
|
});
|
|
@@ -7421,7 +7577,7 @@ function createDomain(database, services = {}) {
|
|
|
7421
7577
|
}
|
|
7422
7578
|
async function readEvents(identity, cursor, projectId) {
|
|
7423
7579
|
if (!hasScope(identity, "read")) fail(403, "insufficient_scope", "Read permission is required");
|
|
7424
|
-
const allowed = await accessibleProjects(identity), named = projectId ? await database.db.select({ id: projects.id }).from(projects).where(projectMatch(projectId))
|
|
7580
|
+
const allowed = await accessibleProjects(identity), named = !projectId ? [] : namesRemote(projectId) ? [await projectByRemote(database.db, identity, projectId)] : await database.db.select({ id: projects.id }).from(projects).where(projectMatch(projectId));
|
|
7425
7581
|
const ids = allowed.filter((row) => !projectId || named.some((match) => match.id === row.id)).map((row) => row.id);
|
|
7426
7582
|
if (projectId && !ids.length) fail(404, "not_found", "Project was not found");
|
|
7427
7583
|
const clock = one(await database.db.select().from(eventClock).where(eq(eventClock.id, 1)));
|
|
@@ -8102,4 +8258,4 @@ function createAuth(database, config) {
|
|
|
8102
8258
|
};
|
|
8103
8259
|
}
|
|
8104
8260
|
//#endregion
|
|
8105
|
-
export {
|
|
8261
|
+
export { pushDevices as $, requireInstanceAdmin as A, createPollingJob as B, defaultSenders as C, receiveUpload as D, createHttpSender as E, createBlobStorage as F, eventClock as G, attachments as H, decodeImage as I, members as J, events as K, imageResponse as L, readCalendarFeed as M, createTicketSearch as N, createAccess as O, createTicketIndex as P, projects as Q, stagedUploadCutoff as R, receiveRepositoryWebhook as S, DeliveryError as T, blobObjects as U, attachmentContent as V, deliveryCheckpoints as W, notifications as X, notificationPreferences as Y, profilePhotos as Z, one as _, deliverySecrets as a, workspaces as at, requireRow as b, ownerBlocker as c, createDomain as d, stagedUploads as et, errorEnvelope as f, notify as g, json as h, createInstance as i, workspaceMembers as it, requireSession as j, createWriteLimiter as k, appendEvents as l, intakeOnly as m, emailCodesAvailable as n, subscriptions as nt, otherActiveAdministrator as o, matchesOIDCBinding as ot, fail as p, instanceSettings as q, signInProviders as r, tickets as rt, ownedWorkspaces as s, createAuth as t, storageBackends as tt, assertVersion as u, reachable as v, deliverBatch as w, processImports as x, recipientColumns as y, createObjectStore as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Dt as streamErrorSchema, G as notificationIdsInput, J as notificationReadInput, K as notificationPreferencesInput, O as eventSchema, Q as operations, St as snapshotSchema, T as errorSchema, X as okSchema, Y as notificationsSchema, ct as publicOperations, et as parseOutput, o as checkpointSchema, q as notificationPreferencesSchema, t as ApiError, tt as parsePublicOutput } from "./src-D70GmZ5R.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { constants } from "node:fs";
|
|
@@ -653,7 +653,7 @@ profile list | profile remove <name> --yes
|
|
|
653
653
|
--input <json|-> Operation input as a JSON object; - reads stdin
|
|
654
654
|
--title <text> Ticket title
|
|
655
655
|
--body <markdown> Comment body
|
|
656
|
-
--expected-version <n> Required version for update, delete
|
|
656
|
+
--expected-version <n> Required version for update, delete and rescheduling recovery; optional for acquire
|
|
657
657
|
--changes <json> Ticket update changes as a JSON object
|
|
658
658
|
--request-id <uuid> Reuse for a deliberate retry across invocations
|
|
659
659
|
--json Compact JSON output (default: indented JSON)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Gt as searchSettingsSchema, Ut as s3SettingsSchema, g as defaultClaimMaxLeaseSeconds, s as claimLeaseSecondsSchema, zt as brokerSettingsSchema } from "./src-D70GmZ5R.mjs";
|
|
2
2
|
import pg from "pg";
|
|
3
3
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
4
4
|
import { relations } from "drizzle-orm";
|