@maudecode/kanvil 0.5.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-Bm6cIseE.mjs → auth-D43XyHdK.mjs} +266 -68
- package/apps/server/dist/client.mjs +2 -2
- package/apps/server/dist/{config-CrJuk5mY.mjs → config-CQVdVhXv.mjs} +1 -1
- package/apps/server/dist/index.mjs +167 -45
- package/apps/server/dist/migrate.mjs +1 -1
- package/apps/server/dist/recover.mjs +3 -3
- package/apps/server/dist/{src-D80LNZKP.mjs → src-D70GmZ5R.mjs} +170 -28
- package/apps/server/drizzle/0039_intake_credential.sql +2 -0
- 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/0039_snapshot.json +7020 -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 +21 -0
- package/apps/web/dist/assets/{CompositeItem-Tueq2VKR.js → CompositeItem-DLTRsada.js} +1 -1
- package/apps/web/dist/assets/{Trans-BgU0eWAX.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-CSpoH6mS.js → archive-restore-BO1qng5Y.js} +1 -1
- package/apps/web/dist/assets/az-AZ-SXqDBPA2.js +1 -0
- package/apps/web/dist/assets/{az-DUcDY8Bp.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-C3OGlXIB.js → corner-left-up-Be7Z9Uhf.js} +1 -1
- package/apps/web/dist/assets/{de-89C1V8Ut.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-D6CICkhi.js → el-Cp69kezM.js} +1 -1
- package/apps/web/dist/assets/el-GR-D1ntYXIt.js +1 -0
- package/apps/web/dist/assets/{ellipsis-lJJU8zB7.js → ellipsis-B0AE8ax3.js} +1 -1
- package/apps/web/dist/assets/{es-B20Izib2.js → es-CwOMVSTx.js} +1 -1
- package/apps/web/dist/assets/es-ES-CllsaxcB.js +1 -0
- package/apps/web/dist/assets/{forms--87R7VQW.js → forms-CkVpTx29.js} +1 -1
- package/apps/web/dist/assets/{fr-DVlGnGsu.js → fr-Cy4co9ic.js} +1 -1
- package/apps/web/dist/assets/fr-FR-DxTw6LgI.js +1 -0
- package/apps/web/dist/assets/{hi-D1F5Pi_I.js → hi-DbO9TKwg.js} +1 -1
- package/apps/web/dist/assets/hi-IN-CaQga-0l.js +1 -0
- package/apps/web/dist/assets/{highlight-ygDJPvRy.js → highlight-zMg1BK-r.js} +1 -1
- package/apps/web/dist/assets/{id-LESXKW7G.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-Dp40BfHj.js +1 -0
- package/apps/web/dist/assets/it-IT-BckuH9Nl.js +1 -0
- package/apps/web/dist/assets/{it-CgRGAs6u.js → it-ilGYCq2T.js} +1 -1
- package/apps/web/dist/assets/item-DQR89bKm.js +1 -0
- package/apps/web/dist/assets/{ja-B7arhzgX.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-Ro_UonyA.js → ko-ajkkoJ_d.js} +1 -1
- package/apps/web/dist/assets/label-settings-ChnKkA97.js +1 -0
- package/apps/web/dist/assets/{markdown-blocks-toSPlHSc.js → markdown-blocks-CpzrwyQG.js} +1 -1
- package/apps/web/dist/assets/member-directory-XK0ZEGUq.js +1 -0
- package/apps/web/dist/assets/{mk-DR3kmrIp.js → mk-C_sMyEHX.js} +1 -1
- package/apps/web/dist/assets/mk-MK-BTa1NMAF.js +1 -0
- package/apps/web/dist/assets/{nl-D3uRJJLT.js → nl-D9-P4UPR.js} +1 -1
- package/apps/web/dist/assets/nl-NL-CQduhDqs.js +1 -0
- package/apps/web/dist/assets/{pl-CtzDOLdV.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-FarXHHuf.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-Byx7O3RH.js → rotate-ccw-Cxh9ec2s.js} +1 -1
- package/apps/web/dist/assets/{ru-DHMEVxvB.js → ru-DxJ6vF4E.js} +1 -1
- package/apps/web/dist/assets/ru-RU-CZKEe7P8.js +1 -0
- package/apps/web/dist/assets/search-DfbsqO7M.js +85 -0
- package/apps/web/dist/assets/{send-DlGV2XQS.js → send-DrJl0BKm.js} +1 -1
- package/apps/web/dist/assets/settings-BN8uIiBG.js +1 -0
- package/apps/web/dist/assets/{sliders-horizontal-CEHGGft_.js → sliders-horizontal-DM47MLe7.js} +1 -1
- package/apps/web/dist/assets/switch-CJTVGtE9.js +1 -0
- package/apps/web/dist/assets/{table-2AEfwytk.js → table-BLLjFPWD.js} +1 -1
- package/apps/web/dist/assets/ticket-detail-BTy_3JeU.js +2 -0
- package/apps/web/dist/assets/ticket-editor-Bb_lyxnJ.js +2 -0
- package/apps/web/dist/assets/ticket-list-Botp5qf5.js +6 -0
- package/apps/web/dist/assets/ticket-list-controls-Dmkno4pm.js +1 -0
- package/apps/web/dist/assets/{tr-CjdSDu0z.js → tr-9IkdRLIk.js} +1 -1
- package/apps/web/dist/assets/tr-TR-Y8wBLO8I.js +1 -0
- package/apps/web/dist/assets/{uk-C8od6u_H.js → uk-Btampsdw.js} +1 -1
- package/apps/web/dist/assets/uk-UA-CVpasGUS.js +1 -0
- package/apps/web/dist/assets/{upload-BeTAGU_W.js → upload-HARy4VC_.js} +1 -1
- package/apps/web/dist/assets/{useQueries-BtRgTciJ.js → useQueries-CTeEF--U.js} +1 -1
- package/apps/web/dist/assets/{vi-BwdBtgfS.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-DKBPjqV-.js +208 -0
- package/apps/web/dist/assets/workspace-settings-DqzBNw60.js +1 -0
- package/apps/web/dist/assets/{zh-CN-DM-UoFmL.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-q2nrwFjt.js → zh-TW-CEIDGTWe.js} +1 -1
- package/apps/web/dist/assets/zh-TW-DCCYakXz.js +1 -0
- package/apps/web/dist/index.html +9 -9
- package/docs/api.md +35 -5
- package/docs/cli.md +1 -1
- package/docs/mobile.md +67 -4
- package/docs/releasing.md +6 -4
- package/package.json +1 -1
- package/apps/web/dist/assets/admin-settings-Ckis3XJG.js +0 -1
- package/apps/web/dist/assets/az-AZ-bFxpMMZN.js +0 -1
- package/apps/web/dist/assets/de-DE-BeXI30Ll.js +0 -1
- package/apps/web/dist/assets/default-Dnep8Yat.js +0 -56
- package/apps/web/dist/assets/delivery-settings-cMldZ9K7.js +0 -1
- package/apps/web/dist/assets/el-GR-SgRHlWhY.js +0 -1
- package/apps/web/dist/assets/es-ES-BF1J5vap.js +0 -1
- package/apps/web/dist/assets/fr-FR-D_sldwf6.js +0 -1
- package/apps/web/dist/assets/hi-IN-CfOw4TWf.js +0 -1
- package/apps/web/dist/assets/id-ID-CLKtHO7Q.js +0 -1
- package/apps/web/dist/assets/index-Bn-MADyj.css +0 -2
- package/apps/web/dist/assets/index-Ds3IHeIi.js +0 -17
- package/apps/web/dist/assets/invitations-C5WbdDUQ.js +0 -1
- package/apps/web/dist/assets/it-IT-eVZNW5qX.js +0 -1
- package/apps/web/dist/assets/item-BD1Poxfs.js +0 -1
- package/apps/web/dist/assets/ja-JP-D96njn-P.js +0 -1
- package/apps/web/dist/assets/ko-KR-Bash_ieA.js +0 -1
- package/apps/web/dist/assets/label-settings-BaikCfal.js +0 -1
- package/apps/web/dist/assets/member-directory-BjUy1KuK.js +0 -1
- package/apps/web/dist/assets/mk-MK-BWQWGIRx.js +0 -1
- package/apps/web/dist/assets/nl-NL-D1tvo0Gd.js +0 -1
- package/apps/web/dist/assets/pl-PL-DNgGUEcQ.js +0 -1
- package/apps/web/dist/assets/project-lifecycle-BBHcRi6p.js +0 -1
- package/apps/web/dist/assets/project-settings-BaYuxYjg.js +0 -1
- package/apps/web/dist/assets/pt-BR-CZsUZQ6P.js +0 -1
- package/apps/web/dist/assets/public-project-DGf3nkg2.js +0 -1
- package/apps/web/dist/assets/ru-RU-4ew92YE_.js +0 -1
- package/apps/web/dist/assets/search-DwsPEC4E.js +0 -85
- package/apps/web/dist/assets/settings-C_sN8Y1h.js +0 -1
- package/apps/web/dist/assets/switch-B_rsZYBf.js +0 -1
- package/apps/web/dist/assets/ticket-detail-C2l8nwF-.js +0 -2
- package/apps/web/dist/assets/ticket-editor-B0UB98HS.js +0 -2
- package/apps/web/dist/assets/ticket-list-D_MHudj1.js +0 -6
- package/apps/web/dist/assets/ticket-list-controls-jKg4i-NA.js +0 -1
- package/apps/web/dist/assets/tr-TR-CM381XCw.js +0 -1
- package/apps/web/dist/assets/uk-UA-AeOiWCVo.js +0 -1
- package/apps/web/dist/assets/vi-VN-td08xXv9.js +0 -1
- package/apps/web/dist/assets/visual-editor-CzB_qwvB.js +0 -208
- package/apps/web/dist/assets/workspace-settings-DOr9j8X1.js +0 -1
- package/apps/web/dist/assets/zh-CN-D3dhEYLS.js +0 -1
- package/apps/web/dist/assets/zh-TW-CfhOEFo8.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),
|
|
@@ -218,6 +220,7 @@ const tickets = pgTable("tickets", {
|
|
|
218
220
|
automationReady: boolean("automation_ready").notNull().default(false),
|
|
219
221
|
claimRequired: boolean("claim_required").notNull().default(false),
|
|
220
222
|
holdReason: text("hold_reason"),
|
|
223
|
+
intakeCredentialId: text("intake_credential_id"),
|
|
221
224
|
startAt: timestamp("start_at", { withTimezone: true }),
|
|
222
225
|
dueAt: timestamp("due_at", { withTimezone: true }),
|
|
223
226
|
archived: boolean("archived").notNull().default(false),
|
|
@@ -233,6 +236,7 @@ const tickets = pgTable("tickets", {
|
|
|
233
236
|
index("ticket_parent").on(table.parentId),
|
|
234
237
|
index("ticket_project_position").on(table.projectId, table.position),
|
|
235
238
|
index("ticket_state").on(table.stateId),
|
|
239
|
+
index("ticket_intake_credential").on(table.intakeCredentialId).where(sql`${table.intakeCredentialId} IS NOT NULL`),
|
|
236
240
|
foreignKey({
|
|
237
241
|
name: "ticket_workflow_state",
|
|
238
242
|
columns: [
|
|
@@ -302,6 +306,7 @@ const receipts = pgTable("mutation_receipts", {
|
|
|
302
306
|
operation: text("operation").notNull(),
|
|
303
307
|
hash: text("hash").notNull(),
|
|
304
308
|
result: jsonb("result").notNull(),
|
|
309
|
+
projectId: uuid("project_id"),
|
|
305
310
|
createdAt: time("created_at")
|
|
306
311
|
}, (table) => [primaryKey({ columns: [table.actorId, table.requestId] })]);
|
|
307
312
|
const storageBackends = pgTable("storage_backends", {
|
|
@@ -367,6 +372,20 @@ const stagedUploads = pgTable("staged_uploads", {
|
|
|
367
372
|
size: integer("size").notNull(),
|
|
368
373
|
createdAt: time("created_at")
|
|
369
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)]);
|
|
370
389
|
const bytea = customType({ dataType: () => "bytea" });
|
|
371
390
|
const attachmentContent = pgTable("attachment_content", {
|
|
372
391
|
id: uuid("id").primaryKey().references(() => attachments.id, { onDelete: "cascade" }),
|
|
@@ -711,8 +730,7 @@ const createObjectStore = (configuration) => {
|
|
|
711
730
|
if (!create) throw new Error("Unsupported object storage provider");
|
|
712
731
|
return create(configuration.config);
|
|
713
732
|
};
|
|
714
|
-
|
|
715
|
-
//#region src/blob-storage.ts
|
|
733
|
+
const intakeAttachmentLifetime = 2592e6;
|
|
716
734
|
const stagedUploadCutoff = () => new Date(Date.now() - stagedUploadLifetime);
|
|
717
735
|
const digest$1 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
718
736
|
function decodeBlobData(value, limit) {
|
|
@@ -811,8 +829,34 @@ function createBlobStorage(database, instance, factory = createObjectStore, sche
|
|
|
811
829
|
});
|
|
812
830
|
}
|
|
813
831
|
async function collectBatch() {
|
|
832
|
+
await database.db.transaction(async (tx) => {
|
|
833
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(7361080)`);
|
|
834
|
+
const expired = await tx.select({
|
|
835
|
+
attachment: attachments,
|
|
836
|
+
key: tickets.key,
|
|
837
|
+
title: tickets.title
|
|
838
|
+
}).from(attachments).innerJoin(tickets, eq(tickets.id, attachments.ticketId)).where(and(isNotNull(tickets.intakeCredentialId), eq(tickets.holdReason, intakeHoldReason), lt(attachments.createdAt, /* @__PURE__ */ new Date(Date.now() - intakeAttachmentLifetime)))).orderBy(asc(attachments.createdAt), asc(attachments.id)).limit(100);
|
|
839
|
+
if (!expired.length) return;
|
|
840
|
+
await tx.delete(attachments).where(inArray(attachments.id, expired.map(({ attachment }) => attachment.id)));
|
|
841
|
+
await appendEvents(tx, expired.map(({ attachment, key, title }) => ({
|
|
842
|
+
projectId: attachment.projectId,
|
|
843
|
+
ticketId: attachment.ticketId,
|
|
844
|
+
actorId: null,
|
|
845
|
+
type: "attachment.deleted",
|
|
846
|
+
data: {
|
|
847
|
+
attachmentId: attachment.id,
|
|
848
|
+
key,
|
|
849
|
+
title,
|
|
850
|
+
source: {
|
|
851
|
+
kind: "system",
|
|
852
|
+
id: "blob-cleanup"
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
})));
|
|
856
|
+
});
|
|
814
857
|
return database.db.transaction(async (tx) => {
|
|
815
858
|
await tx.delete(stagedUploads).where(lt(stagedUploads.createdAt, stagedUploadCutoff()));
|
|
859
|
+
await tx.delete(uploadUrls).where(lt(uploadUrls.expiresAt, /* @__PURE__ */ new Date()));
|
|
816
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", {
|
|
817
861
|
of: blobObjects,
|
|
818
862
|
skipLocked: true
|
|
@@ -1065,14 +1109,23 @@ function parseQuery(query) {
|
|
|
1065
1109
|
terms: query
|
|
1066
1110
|
};
|
|
1067
1111
|
}
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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`;
|
|
1076
1129
|
const builtinSearch = { where: async (_db, _ids, query) => ({
|
|
1077
1130
|
where: postgresSearch(query),
|
|
1078
1131
|
rank: postgresRank(query),
|
|
@@ -1138,13 +1191,17 @@ function createTicketSearch(database, instance, factory = createTicketIndex, sch
|
|
|
1138
1191
|
if (!state || !clock || state.error || state.configHash !== fingerprint(settings) || state.cursor !== clock.value) return fallback();
|
|
1139
1192
|
const root = await instance.read();
|
|
1140
1193
|
try {
|
|
1194
|
+
const parsed = parseQuery(query);
|
|
1141
1195
|
const hits = await factory({
|
|
1142
1196
|
provider: "elasticsearch",
|
|
1143
1197
|
config: settings
|
|
1144
|
-
}).find(root.identity, projectIds,
|
|
1198
|
+
}).find(root.identity, projectIds, parsed);
|
|
1145
1199
|
const ids = hits.map((hit) => hit.id), ranks = hits.map((hit) => hit.rank);
|
|
1146
1200
|
return {
|
|
1147
|
-
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`,
|
|
1148
1205
|
rank: sql`(${`{${ranks.join(",")}}`}::int[])[array_position(${`{${ids.join(",")}}`}::uuid[], ${tickets.id})]`,
|
|
1149
1206
|
engine: "index",
|
|
1150
1207
|
indexing: false
|
|
@@ -2387,14 +2444,19 @@ function predicateSQL({ field, operator, values }) {
|
|
|
2387
2444
|
const overlap = sql`${tickets.labels} && ${sql.param(values)}::text[]`;
|
|
2388
2445
|
return operator === "any" ? overlap : sql`NOT (${overlap})`;
|
|
2389
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
|
+
}
|
|
2390
2452
|
const column = field === "assigneeId" ? tickets.assigneeId : filterColumns[field];
|
|
2391
2453
|
const named = field === "assigneeId" ? values.filter((value) => value !== "none") : values, unset = field === "assigneeId" && values.includes("none");
|
|
2392
2454
|
if (operator === "any") return or(named.length ? inArray(column, named) : void 0, unset ? isNull(column) : void 0);
|
|
2393
2455
|
return and(named.length ? or(isNull(column), notInArray(column, named)) : void 0, unset ? isNotNull(column) : void 0);
|
|
2394
2456
|
}
|
|
2395
2457
|
const predicateWhere = (filter) => and(...ticketPredicates(filter).map(predicateSQL));
|
|
2396
|
-
function ticketOrder(
|
|
2397
|
-
const direction =
|
|
2458
|
+
function ticketOrder({ sort = "priority", order }) {
|
|
2459
|
+
const direction = order ?? defaultOrder[sort];
|
|
2398
2460
|
const by = (column) => direction === "asc" ? sql`${column} ASC NULLS LAST` : sql`${column} DESC NULLS LAST`;
|
|
2399
2461
|
return [{
|
|
2400
2462
|
priority: by(rank(tickets.priority, priorities)),
|
|
@@ -2410,7 +2472,28 @@ function ticketOrder(input) {
|
|
|
2410
2472
|
kind: by(tickets.kind),
|
|
2411
2473
|
assignee: by(sql`(SELECT lower(name) FROM "user" WHERE id = ${tickets.assigneeId})`),
|
|
2412
2474
|
manual: by(tickets.position)
|
|
2413
|
-
}[
|
|
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
|
+
};
|
|
2414
2497
|
}
|
|
2415
2498
|
async function pageOf(ctx, where, order, input) {
|
|
2416
2499
|
const rows = await ctx.db.select({
|
|
@@ -2453,7 +2536,8 @@ async function createTicketRow(ctx, input) {
|
|
|
2453
2536
|
customFields: intakeOnly(ctx.actor) ? {} : applyCustomFieldValues(await projectCustomFields(ctx), null, customFields),
|
|
2454
2537
|
...await allocateTicket(ctx),
|
|
2455
2538
|
id,
|
|
2456
|
-
createdBy: ctx.actor.id
|
|
2539
|
+
createdBy: ctx.actor.id,
|
|
2540
|
+
intakeCredentialId: intakeOnly(ctx.actor) ? ctx.actor.ownerId : null
|
|
2457
2541
|
}).returning());
|
|
2458
2542
|
await subscribe(ctx, row.projectId, row.id, [ctx.actor.id, row.assigneeId]);
|
|
2459
2543
|
return {
|
|
@@ -2500,12 +2584,16 @@ const hitRow = z.object({
|
|
|
2500
2584
|
const summarize = (ticket) => ticketSummarySchema.parse(json(ticket));
|
|
2501
2585
|
const snippetLength = 240;
|
|
2502
2586
|
function snippet(body, query) {
|
|
2503
|
-
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);
|
|
2504
2588
|
let at = -1, size = 0;
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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);
|
|
2508
2595
|
}
|
|
2596
|
+
for (const needle of [terms, ...terms.split(" ")]) if (needle) closest(lower.indexOf(needle.toLowerCase()), needle.length);
|
|
2509
2597
|
if (at < 0) return "";
|
|
2510
2598
|
if (text.length <= snippetLength) return text;
|
|
2511
2599
|
const start = Math.max(0, Math.min(at + Math.floor((size - snippetLength) / 2), text.length - snippetLength)), end = start + snippetLength;
|
|
@@ -2637,9 +2725,9 @@ const ticketHandlers = {
|
|
|
2637
2725
|
async list_tickets(ctx, input) {
|
|
2638
2726
|
const parent = input.parentId ? await getTicket(ctx, input.parentId) : void 0;
|
|
2639
2727
|
const related = input.relatedTo ? await getTicket(ctx, input.relatedTo) : void 0;
|
|
2640
|
-
const search =
|
|
2728
|
+
const search = await listQuery(ctx, [project(ctx).id], input);
|
|
2641
2729
|
await assertCustomFieldFilters(ctx, [project(ctx).id], input);
|
|
2642
|
-
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);
|
|
2643
2731
|
return input.view === "summary" ? {
|
|
2644
2732
|
...page,
|
|
2645
2733
|
items: page.items.map(summarize)
|
|
@@ -2652,11 +2740,11 @@ const ticketHandlers = {
|
|
|
2652
2740
|
total: 0,
|
|
2653
2741
|
activity: []
|
|
2654
2742
|
};
|
|
2655
|
-
const ids = allowed.map((row) => row.id);
|
|
2656
|
-
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);
|
|
2657
2745
|
const activity = input.activity ? await ctx.db.select().from(events).where(inArray(events.projectId, ids)).orderBy(desc(events.id)).limit(input.activity) : [];
|
|
2658
2746
|
return {
|
|
2659
|
-
...await pageOf(ctx, where,
|
|
2747
|
+
...await pageOf(ctx, where, search.order, input),
|
|
2660
2748
|
activity
|
|
2661
2749
|
};
|
|
2662
2750
|
},
|
|
@@ -2871,7 +2959,7 @@ const ticketHandlers = {
|
|
|
2871
2959
|
const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
|
|
2872
2960
|
if (!allowed.length) return { items: [] };
|
|
2873
2961
|
const ids = allowed.map((row) => row.id), match = await ctx.search.where(ctx.db, ids, input.q);
|
|
2874
|
-
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) => {
|
|
2875
2963
|
const owner = allowed.find((row) => row.id === ticket.projectId);
|
|
2876
2964
|
if (!owner) return [];
|
|
2877
2965
|
const summary = input.view === "summary", body = `${ticket.description} ${ticket.acceptanceCriteria}`;
|
|
@@ -2991,7 +3079,7 @@ const ticketHandlers = {
|
|
|
2991
3079
|
},
|
|
2992
3080
|
async claim_ticket(ctx, input) {
|
|
2993
3081
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
2994
|
-
assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
|
|
3082
|
+
if (input.expectedVersion !== void 0) assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
|
|
2995
3083
|
const eligible = await readiness(ctx, ticket);
|
|
2996
3084
|
if (!eligible.ready) fail(409, "not_ready", "The ticket is not ready to claim", eligible);
|
|
2997
3085
|
const claim = one(await ctx.db.insert(claims).values({
|
|
@@ -3013,7 +3101,10 @@ const ticketHandlers = {
|
|
|
3013
3101
|
workerId: claim.workerId,
|
|
3014
3102
|
version: row.version
|
|
3015
3103
|
});
|
|
3016
|
-
return
|
|
3104
|
+
return {
|
|
3105
|
+
...claim,
|
|
3106
|
+
ticket: row
|
|
3107
|
+
};
|
|
3017
3108
|
},
|
|
3018
3109
|
async renew_claim(ctx, input) {
|
|
3019
3110
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
@@ -3384,6 +3475,90 @@ async function addComment(ctx, input) {
|
|
|
3384
3475
|
});
|
|
3385
3476
|
return row;
|
|
3386
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
|
+
}
|
|
3387
3562
|
const collaborationHandlers = {
|
|
3388
3563
|
async list_relations(ctx, input) {
|
|
3389
3564
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
@@ -3493,41 +3668,34 @@ const collaborationHandlers = {
|
|
|
3493
3668
|
};
|
|
3494
3669
|
},
|
|
3495
3670
|
async upload_attachment(ctx, input) {
|
|
3496
|
-
|
|
3497
|
-
const bytes = decodeBlobData(input.data, attachmentLimit);
|
|
3498
|
-
const id = crypto.randomUUID();
|
|
3499
|
-
await ctx.blobs.put(ctx.db, id, bytes, input.contentType);
|
|
3500
|
-
const row = one(await ctx.db.insert(attachments).values({
|
|
3501
|
-
id,
|
|
3502
|
-
objectId: id,
|
|
3503
|
-
projectId: project(ctx).id,
|
|
3504
|
-
ticketId: ticket.id,
|
|
3505
|
-
actorId: ctx.actor.id,
|
|
3506
|
-
name: input.name,
|
|
3507
|
-
contentType: input.contentType,
|
|
3508
|
-
size: bytes.length
|
|
3509
|
-
}).returning());
|
|
3510
|
-
await publish(ctx, "attachment.created", ticket.id, {
|
|
3511
|
-
attachmentId: row.id,
|
|
3512
|
-
size: row.size
|
|
3513
|
-
});
|
|
3514
|
-
return row;
|
|
3671
|
+
return attachFile(ctx, await getTicket(ctx, input.ticketId), crypto.randomUUID(), decodeBlobData(input.data, attachmentLimit), input);
|
|
3515
3672
|
},
|
|
3516
3673
|
async stage_upload(ctx, input) {
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
const
|
|
3521
|
-
|
|
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(),
|
|
3522
3680
|
projectId: project(ctx).id,
|
|
3523
|
-
|
|
3681
|
+
ticketId: ticket?.id ?? null,
|
|
3682
|
+
actorId: id,
|
|
3683
|
+
credential: machine ? {
|
|
3684
|
+
ownerId,
|
|
3685
|
+
scopes: granted,
|
|
3686
|
+
projectIds,
|
|
3687
|
+
apiKey
|
|
3688
|
+
} : null,
|
|
3524
3689
|
name: input.name,
|
|
3525
3690
|
contentType: input.contentType,
|
|
3526
|
-
size:
|
|
3691
|
+
size: input.size,
|
|
3692
|
+
tokenHash: hashUploadToken(token),
|
|
3693
|
+
expiresAt: new Date(Date.now() + uploadUrlLifetime)
|
|
3527
3694
|
}).returning());
|
|
3528
3695
|
return {
|
|
3529
|
-
|
|
3530
|
-
|
|
3696
|
+
uploadId: row.id,
|
|
3697
|
+
url: new URL(`/api/uploads/${token}`, ctx.publicUrl).href,
|
|
3698
|
+
expiresAt: row.expiresAt
|
|
3531
3699
|
};
|
|
3532
3700
|
},
|
|
3533
3701
|
async discard_upload(ctx, input) {
|
|
@@ -5291,6 +5459,11 @@ async function takePrefix(ctx, prefix, projectId) {
|
|
|
5291
5459
|
projectId: owner
|
|
5292
5460
|
}).onConflictDoNothing();
|
|
5293
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
|
+
}
|
|
5294
5467
|
async function storeImage(ctx, image) {
|
|
5295
5468
|
const objectId = crypto.randomUUID();
|
|
5296
5469
|
await ctx.blobs.put(ctx.db, objectId, decodeImage(image), image.contentType);
|
|
@@ -5406,18 +5579,20 @@ const projectHandlers = {
|
|
|
5406
5579
|
const current = project(ctx);
|
|
5407
5580
|
assertVersion(current, input.expectedVersion, "project");
|
|
5408
5581
|
const key = input.key ?? current.key;
|
|
5409
|
-
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);
|
|
5410
5583
|
if (sharing) requirePermission(ctx.permissions, "sharing.manage", "project");
|
|
5411
5584
|
if (details || !sharing) requirePermission(ctx.permissions, "project.manage", "project");
|
|
5412
5585
|
if (key !== current.key) {
|
|
5413
5586
|
await (await takePrefix(ctx, key, current.id))(current.id);
|
|
5414
5587
|
await ctx.db.update(tickets).set({ key: sql`${key}::text || '-' || ${tickets.number}` }).where(eq(tickets.projectId, current.id));
|
|
5415
5588
|
}
|
|
5589
|
+
if (input.repositories && !current.archivedAt) await assertRepositoriesFree(ctx, current.id, current.workspaceId, input.repositories);
|
|
5416
5590
|
const row = one(await ctx.db.update(projects).set({
|
|
5417
5591
|
key,
|
|
5418
5592
|
name: input.name,
|
|
5419
5593
|
...input.visibility ? { visibility: input.visibility } : {},
|
|
5420
5594
|
...input.publicReadEnabled === void 0 ? {} : { publicReadEnabled: input.publicReadEnabled },
|
|
5595
|
+
...input.repositories ? { repositories: input.repositories } : {},
|
|
5421
5596
|
description: input.description,
|
|
5422
5597
|
version: current.version + 1,
|
|
5423
5598
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -5835,6 +6010,7 @@ async function assertTransferable(ctx, workspaceId) {
|
|
|
5835
6010
|
const current = project(ctx);
|
|
5836
6011
|
if (workspaceId === current.workspaceId) fail(400, "validation_error", "The project is already in this workspace");
|
|
5837
6012
|
await requireWorkspace(ctx, current.workspaceId, "project.manage");
|
|
6013
|
+
if (!current.archivedAt) await assertRepositoriesFree(ctx, current.id, workspaceId, current.repositories);
|
|
5838
6014
|
}
|
|
5839
6015
|
async function projectPeople(ctx, workspaceId = project(ctx).workspaceId) {
|
|
5840
6016
|
const current = project(ctx), qualifiedUser = sql`${user}."id"`;
|
|
@@ -5997,6 +6173,7 @@ const lifecycleHandlers = {
|
|
|
5997
6173
|
assertVersion(current, input.expectedVersion, "project", input.archived ? "archiving" : "restoring");
|
|
5998
6174
|
if (!!current.archivedAt === input.archived) return current;
|
|
5999
6175
|
if (input.archived) assertNoBlockers(await claimBlockers(ctx));
|
|
6176
|
+
else await assertRepositoriesFree(ctx, current.id, current.workspaceId, current.repositories);
|
|
6000
6177
|
const row = await changed(ctx, { archivedAt: input.archived ? /* @__PURE__ */ new Date() : null });
|
|
6001
6178
|
await publish(ctx, input.archived ? "project.archived" : "project.restored", null, { version: row.version });
|
|
6002
6179
|
return row;
|
|
@@ -6969,7 +7146,12 @@ const complete = (status) => closedStates.some((closed) => closed === status);
|
|
|
6969
7146
|
const intakeHoldReason = "Untrusted intake: human triage required";
|
|
6970
7147
|
const intakeQuarantined = (ticket) => ticket.holdReason === intakeHoldReason;
|
|
6971
7148
|
const intakeOnly = (actor) => actor.scopes.length > 0 && actor.scopes.every((scope) => scope === "intake");
|
|
6972
|
-
const
|
|
7149
|
+
const intakeOperations = /* @__PURE__ */ new Set([
|
|
7150
|
+
"create_ticket",
|
|
7151
|
+
"upload_attachment",
|
|
7152
|
+
"list_attachments"
|
|
7153
|
+
]);
|
|
7154
|
+
const hasScope = (actor, scope, operation) => intakeOnly(actor) ? !!operation && intakeOperations.has(operation) : actor.scopes.includes("admin") || actor.scopes.includes(scope) || scope === "read" && actor.scopes.some((value) => value !== "intake");
|
|
6973
7155
|
function requireScope(actor, scope, operation) {
|
|
6974
7156
|
if (!hasScope(actor, scope, operation)) fail(403, "insufficient_scope", `This credential requires ${scope} permission`);
|
|
6975
7157
|
}
|
|
@@ -7018,6 +7200,16 @@ const publicActor = {
|
|
|
7018
7200
|
projectIds: []
|
|
7019
7201
|
};
|
|
7020
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
|
+
}
|
|
7021
7213
|
const issuedKey = /^([A-Z][A-Z0-9]{1,9})-([1-9]\d{0,8})$/;
|
|
7022
7214
|
function ticketMatch(identifier) {
|
|
7023
7215
|
const key = identifier.toUpperCase(), [, prefix, number] = issuedKey.exec(key) ?? [];
|
|
@@ -7033,7 +7225,7 @@ async function context(db, actor, scope, projectId, workspaceId, permission = sc
|
|
|
7033
7225
|
ctx.permissions = access.permissions;
|
|
7034
7226
|
}
|
|
7035
7227
|
if (!projectId) return ctx;
|
|
7036
|
-
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));
|
|
7037
7229
|
if (actor.projectIds && !actor.projectIds.includes(row.id)) fail(404, "not_found", "Project was not found");
|
|
7038
7230
|
const access = await projectAccess(db, actor, row);
|
|
7039
7231
|
if (!access) fail(404, "not_found", "Project was not found");
|
|
@@ -7046,7 +7238,9 @@ async function context(db, actor, scope, projectId, workspaceId, permission = sc
|
|
|
7046
7238
|
};
|
|
7047
7239
|
}
|
|
7048
7240
|
async function getTicket(ctx, identifier) {
|
|
7049
|
-
|
|
7241
|
+
const ticket = one(await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, project(ctx).id), ticketMatch(identifier))).limit(1));
|
|
7242
|
+
if (intakeOnly(ctx.actor) && !(ticket.intakeCredentialId === ctx.actor.ownerId && intakeQuarantined(ticket))) fail(404, "not_found", "The requested record was not found");
|
|
7243
|
+
return ticket;
|
|
7050
7244
|
}
|
|
7051
7245
|
const insertBatch = 1e3;
|
|
7052
7246
|
async function insertAll(rows, insert) {
|
|
@@ -7338,10 +7532,13 @@ function createDomain(database, services = {}) {
|
|
|
7338
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;
|
|
7339
7533
|
if (receipt && (receipt.hash !== hash || receipt.operation !== name)) fail(409, "idempotency_conflict", "This requestId was already used for another write");
|
|
7340
7534
|
if (receipt && (name === "delete_project" || name === "delete_workspace")) return parseOutput(name, receipt.result);
|
|
7341
|
-
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);
|
|
7342
7536
|
if (receipt) {
|
|
7343
7537
|
if ((name === "create_time_entry" || name === "update_time_entry" || name === "delete_time_entry") && "ticketId" in input) await getTicket(ctx, input.ticketId);
|
|
7344
|
-
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);
|
|
7345
7542
|
if (name === "create_project") await context(tx, actor, "read", projectSchema.parse(prior).id);
|
|
7346
7543
|
if (name === "create_workspace") await requireWorkspace(ctx, workspaceSchema.parse(prior).id, null);
|
|
7347
7544
|
if (name === "relocate_ticket") await context(tx, actor, "write", ticketSchema.parse(prior).projectId);
|
|
@@ -7354,7 +7551,8 @@ function createDomain(database, services = {}) {
|
|
|
7354
7551
|
requestId,
|
|
7355
7552
|
hash,
|
|
7356
7553
|
operation: name,
|
|
7357
|
-
result
|
|
7554
|
+
result,
|
|
7555
|
+
projectId: ctx.project?.id ?? null
|
|
7358
7556
|
});
|
|
7359
7557
|
return result;
|
|
7360
7558
|
});
|
|
@@ -7379,7 +7577,7 @@ function createDomain(database, services = {}) {
|
|
|
7379
7577
|
}
|
|
7380
7578
|
async function readEvents(identity, cursor, projectId) {
|
|
7381
7579
|
if (!hasScope(identity, "read")) fail(403, "insufficient_scope", "Read permission is required");
|
|
7382
|
-
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));
|
|
7383
7581
|
const ids = allowed.filter((row) => !projectId || named.some((match) => match.id === row.id)).map((row) => row.id);
|
|
7384
7582
|
if (projectId && !ids.length) fail(404, "not_found", "Project was not found");
|
|
7385
7583
|
const clock = one(await database.db.select().from(eventClock).where(eq(eventClock.id, 1)));
|
|
@@ -8060,4 +8258,4 @@ function createAuth(database, config) {
|
|
|
8060
8258
|
};
|
|
8061
8259
|
}
|
|
8062
8260
|
//#endregion
|
|
8063
|
-
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";
|