@maudecode/kanvil 0.4.3 → 0.5.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/.env.example +5 -0
- package/apps/server/dist/{auth-CnAQzKyq.mjs → auth-Cs_whzgk.mjs} +382 -127
- package/apps/server/dist/client.mjs +4 -3
- package/apps/server/dist/{config-DOT1SDQO.mjs → config-DKuNQY5b.mjs} +15 -7
- package/apps/server/dist/index.mjs +105 -57
- package/apps/server/dist/migrate.mjs +2 -2
- package/apps/server/dist/recover.mjs +4 -4
- package/apps/server/dist/{src-VxR5jZiK.mjs → src-DptOUNRd.mjs} +67 -23
- package/apps/web/dist/assets/{CompositeItem-B5Qlh5m0.js → CompositeItem-CXqewtEL.js} +1 -1
- package/apps/web/dist/assets/{Trans-DxqBaUXP.js → Trans-B1rRlWca.js} +1 -1
- package/apps/web/dist/assets/{admin-settings-Bp7V2t1V.js → admin-settings-DE3MfBFE.js} +1 -1
- package/apps/web/dist/assets/{archive-restore-wsAzhqi-.js → archive-restore-76sStk8K.js} +1 -1
- package/apps/web/dist/assets/az-AZ-bFxpMMZN.js +1 -0
- package/apps/web/dist/assets/{az-C4KDjOOw.js → az-B7OgHoaE.js} +1 -1
- package/apps/web/dist/assets/{corner-left-up-arA2ipmM.js → corner-left-up-PD-73Nae.js} +1 -1
- package/apps/web/dist/assets/{de-Z599ZW7b.js → de-B-bv38Q0.js} +1 -1
- package/apps/web/dist/assets/de-DE-BeXI30Ll.js +1 -0
- package/apps/web/dist/assets/default-bQFGTwIp.js +56 -0
- package/apps/web/dist/assets/{delivery-settings-D5NKPlhN.js → delivery-settings-BvMeOsCS.js} +1 -1
- package/apps/web/dist/assets/{el-xsvq992p.js → el-BQHQHQgX.js} +1 -1
- package/apps/web/dist/assets/el-GR-SgRHlWhY.js +1 -0
- package/apps/web/dist/assets/{ellipsis-UOkL14vq.js → ellipsis-GF1HwTDb.js} +1 -1
- package/apps/web/dist/assets/es-ES-BF1J5vap.js +1 -0
- package/apps/web/dist/assets/{es-D3e9L5_i.js → es-f5Xr8ldr.js} +1 -1
- package/apps/web/dist/assets/{forms-BbWAl0BZ.js → forms-BlEL8C_P.js} +1 -1
- package/apps/web/dist/assets/{fr-CY35Bcpp.js → fr-DSR0htnl.js} +1 -1
- package/apps/web/dist/assets/fr-FR-D_sldwf6.js +1 -0
- package/apps/web/dist/assets/{hi-Cqfv-E5P.js → hi-BaAJEWuW.js} +1 -1
- package/apps/web/dist/assets/hi-IN-CfOw4TWf.js +1 -0
- package/apps/web/dist/assets/{highlight-Ds7xUUQv.js → highlight-DXT2xgt8.js} +1 -1
- package/apps/web/dist/assets/id-ID-CLKtHO7Q.js +1 -0
- package/apps/web/dist/assets/{id-CFBOVDUC.js → id-kxogWcip.js} +1 -1
- package/apps/web/dist/assets/{index-2V6MWx63.js → index-CYVrs22O.js} +5 -5
- package/apps/web/dist/assets/{invitations-BPk5fwwU.js → invitations-BPik3J_G.js} +1 -1
- package/apps/web/dist/assets/{it-DNLponcC.js → it-C_qk40U6.js} +1 -1
- package/apps/web/dist/assets/it-IT-eVZNW5qX.js +1 -0
- package/apps/web/dist/assets/{item-DED0Ikqi.js → item-TCHuZNK6.js} +1 -1
- package/apps/web/dist/assets/{ja-ZXGlS4WA.js → ja-Dn5h64V9.js} +1 -1
- package/apps/web/dist/assets/ja-JP-D96njn-P.js +1 -0
- package/apps/web/dist/assets/{ko-CSoj1hQM.js → ko-CjaoEPuG.js} +1 -1
- package/apps/web/dist/assets/ko-KR-Bash_ieA.js +1 -0
- package/apps/web/dist/assets/{label-settings-D12QOkDs.js → label-settings-DZTorNDm.js} +1 -1
- package/apps/web/dist/assets/{markdown-blocks-DbsqN1T2.js → markdown-blocks-BQLO-H_7.js} +1 -1
- package/apps/web/dist/assets/{member-directory-BRhEIYZ6.js → member-directory-Dx_jQYfI.js} +1 -1
- package/apps/web/dist/assets/mk-MK-BWQWGIRx.js +1 -0
- package/apps/web/dist/assets/{mk-D77vceoz.js → mk-jLeGD50T.js} +1 -1
- package/apps/web/dist/assets/{nl-Bew8-xmX.js → nl-AVtC5ygT.js} +1 -1
- package/apps/web/dist/assets/nl-NL-D1tvo0Gd.js +1 -0
- package/apps/web/dist/assets/{pl-CEBl_oa9.js → pl-Dup1h_Y1.js} +1 -1
- package/apps/web/dist/assets/pl-PL-DNgGUEcQ.js +1 -0
- package/apps/web/dist/assets/{project-lifecycle-DGHAmEB2.js → project-lifecycle-n1ZZbYfu.js} +1 -1
- package/apps/web/dist/assets/{project-settings-DY8Ossd-.js → project-settings-B_Ih5kiz.js} +1 -1
- package/apps/web/dist/assets/pt-BR-CZsUZQ6P.js +1 -0
- package/apps/web/dist/assets/{pt-BR-BDTQeKcp.js → pt-BR-Cm04EWM-.js} +1 -1
- package/apps/web/dist/assets/{public-project-BeXla8TL.js → public-project-BivDjU_a.js} +1 -1
- package/apps/web/dist/assets/{rotate-ccw-DGM890Fc.js → rotate-ccw-CSaKhZYt.js} +1 -1
- package/apps/web/dist/assets/{ru-BeH0eXQq.js → ru-IeycXsyE.js} +1 -1
- package/apps/web/dist/assets/ru-RU-4ew92YE_.js +1 -0
- package/apps/web/dist/assets/{search-B3-w3oCd.js → search-Ble6WSEU.js} +3 -3
- package/apps/web/dist/assets/{send-B9baaCnt.js → send-BLMZ8gQ0.js} +1 -1
- package/apps/web/dist/assets/settings-CH48qQK2.js +1 -0
- package/apps/web/dist/assets/{sliders-horizontal-yKgghk8h.js → sliders-horizontal-Db6kRvuw.js} +1 -1
- package/apps/web/dist/assets/{switch-ByOrazEW.js → switch-BeX-0Fwq.js} +1 -1
- package/apps/web/dist/assets/{table-Cm6NQXfq.js → table-UJp5S9yy.js} +1 -1
- package/apps/web/dist/assets/ticket-detail-C24opfNW.js +2 -0
- package/apps/web/dist/assets/{ticket-editor-CLDBUk0n.js → ticket-editor--d_bG9C1.js} +2 -2
- package/apps/web/dist/assets/{ticket-list-BC9acNl4.js → ticket-list-DRbZWYHf.js} +1 -1
- package/apps/web/dist/assets/{ticket-list-controls-CRQk5ghe.js → ticket-list-controls-fYBottFC.js} +1 -1
- package/apps/web/dist/assets/{tr-BNApk6Hc.js → tr-DtT_dKUK.js} +1 -1
- package/apps/web/dist/assets/tr-TR-CM381XCw.js +1 -0
- package/apps/web/dist/assets/{uk-CjShsgpN.js → uk-CsbeHSS6.js} +1 -1
- package/apps/web/dist/assets/uk-UA-AeOiWCVo.js +1 -0
- package/apps/web/dist/assets/{upload-BzLq4CMS.js → upload-DUEg04Nj.js} +1 -1
- package/apps/web/dist/assets/{useQueries-D64vLhM6.js → useQueries-5lW2C5Sb.js} +1 -1
- package/apps/web/dist/assets/vi-VN-td08xXv9.js +1 -0
- package/apps/web/dist/assets/{vi-s5Hfv1Y-.js → vi-rkf1BOLx.js} +1 -1
- package/apps/web/dist/assets/{visual-editor-KE96mwCg.js → visual-editor-DkFLjouD.js} +1 -1
- package/apps/web/dist/assets/{workspace-settings-C-Phmi9i.js → workspace-settings-Bcsp-VoJ.js} +1 -1
- package/apps/web/dist/assets/zh-CN-D3dhEYLS.js +1 -0
- package/apps/web/dist/assets/{zh-CN-BBnpHJ9R.js → zh-CN-Xo-QId5w.js} +1 -1
- package/apps/web/dist/assets/zh-TW-CfhOEFo8.js +1 -0
- package/apps/web/dist/assets/{zh-TW-sSWkqSLN.js → zh-TW-DEnTqjbn.js} +1 -1
- package/apps/web/dist/index.html +7 -7
- package/docs/api.md +27 -13
- package/docs/cli.md +9 -1
- package/docs/releasing.md +16 -2
- package/docs/self-hosting.md +8 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/az-AZ-DlsPlUZi.js +0 -1
- package/apps/web/dist/assets/de-DE-DLCaqUUe.js +0 -1
- package/apps/web/dist/assets/default-6QIAyMq2.js +0 -56
- package/apps/web/dist/assets/el-GR-P2XcwrSU.js +0 -1
- package/apps/web/dist/assets/es-ES-CJoX8f3w.js +0 -1
- package/apps/web/dist/assets/fr-FR-yLkQdy6a.js +0 -1
- package/apps/web/dist/assets/hi-IN-y8dqej7z.js +0 -1
- package/apps/web/dist/assets/id-ID-DSvh8mGH.js +0 -1
- package/apps/web/dist/assets/it-IT-BUcfUqNo.js +0 -1
- package/apps/web/dist/assets/ja-JP-C-jLP_Ay.js +0 -1
- package/apps/web/dist/assets/ko-KR-C5Ozdm2E.js +0 -1
- package/apps/web/dist/assets/mk-MK-BdM_SsE8.js +0 -1
- package/apps/web/dist/assets/nl-NL-CwjB5sva.js +0 -1
- package/apps/web/dist/assets/pl-PL-6-PMhTBd.js +0 -1
- package/apps/web/dist/assets/pt-BR-TD_wt6Hr.js +0 -1
- package/apps/web/dist/assets/ru-RU-Bk1a6Oln.js +0 -1
- package/apps/web/dist/assets/settings-BKZfEkVk.js +0 -1
- package/apps/web/dist/assets/ticket-detail-Cd3_e2jz.js +0 -2
- package/apps/web/dist/assets/tr-TR-CUfBb7K9.js +0 -1
- package/apps/web/dist/assets/uk-UA-BDaXBsDP.js +0 -1
- package/apps/web/dist/assets/vi-VN-Bgb-L6mE.js +0 -1
- package/apps/web/dist/assets/zh-CN-BypkvBe6.js +0 -1
- package/apps/web/dist/assets/zh-TW-CgMsT_XS.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
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 permissionLabels, A as hasAdminRole, At as
|
|
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-DKuNQY5b.mjs";
|
|
2
|
+
import { $ as permissionLabels, A as hasAdminRole, At as workflowOrder, Bt as searchSettingsSchema, D as filterKey, Dt as ticketPredicates, E as eventSchema, Et as ticketImportSchema, F as isHexColor, Ft as instanceUpdateSchema, J as operationPermission, N as importSecretFields, Nt as authenticationPolicySchema, O as filterList, Ot as ticketSchema, P as invitationStatuses, Pt as brokerSettingsSchema, Q as parsePublicOutput, Rt as s3SettingsSchema, S as difficulties, St as states, T as eventLabels, Tt as ticketFilter, V as mentionScheme, X as parseCustomFieldFilter, Y as operations, Z as parseOutput, _ as deletedAccountName, _t as seededPermissions, at as publicOperations, b as destinationEventKeys, bt as stagedUploadLifetime, ct as repositoryProviderDefinitions, d as customFieldSchema, dt as resourceProvider, et as permissionScope, f as customFieldValue, ft as roleCategory, g as defaultWorkflow, gt as searchResultTypes, h as defaultOrder, ht as scopes, i as channelRouteAllows, it as projectSchema, jt as workspaceSchema, k as formatEvent, kt as ticketSummarySchema, l as credentialProjectsSchema, lt as repositoryProviders, m as defaultClaimMaxLeaseSeconds, mt as roles, n as attachmentLimit, nt as priorities, p as customFieldValueSchema, pt as roleLabels, r as channelKinds, rt as priorityLabels, s as closedStates, st as remoteKinds, t as ApiError, tt as permissions, u as customFieldFilterTokens, ut as repositorySecretFields, v as deliverySecretFields, vt as signInMethodLabels, w as eventCategories, wt as ticketExportFormat, x as destinationKinds, xt as stateLabels, y as deliverySettingsSchema, yt as snapshotSchema, z as isSeededRole, zt as searchAuthentication } from "./src-DptOUNRd.mjs";
|
|
3
3
|
import { and, asc, count, desc, eq, getTableColumns, gt, ilike, 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";
|
|
@@ -15,6 +15,7 @@ import { request as request$1 } from "node:https";
|
|
|
15
15
|
import { BlockList, isIP } from "node:net";
|
|
16
16
|
import nodemailer from "nodemailer";
|
|
17
17
|
import { APIError, createAuthMiddleware } from "better-auth/api";
|
|
18
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
19
|
import { betterAuth } from "better-auth";
|
|
19
20
|
import { admin, genericOAuth, jwt } from "better-auth/plugins";
|
|
20
21
|
import { emailOTP } from "better-auth/plugins/email-otp";
|
|
@@ -769,7 +770,7 @@ function createBlobStorage(database, instance, factory = createObjectStore, sche
|
|
|
769
770
|
skipLocked: true
|
|
770
771
|
});
|
|
771
772
|
for (const row of rows) {
|
|
772
|
-
const backend = await instance.backend(requireRow(row.backendId ?? void 0)), key = requireRow(row.objectKey ?? void 0), bytes = requireRow(row.data ?? void 0);
|
|
773
|
+
const backend = await instance.backend(requireRow(row.backendId ?? void 0), tx), key = requireRow(row.objectKey ?? void 0), bytes = requireRow(row.data ?? void 0);
|
|
773
774
|
await transfer(backend.spec, async (store) => {
|
|
774
775
|
await store.put(key, bytes, row.contentType);
|
|
775
776
|
const verified = await store.get(key, row.size);
|
|
@@ -818,7 +819,7 @@ function createBlobStorage(database, instance, factory = createObjectStore, sche
|
|
|
818
819
|
});
|
|
819
820
|
for (const { object } of rows) {
|
|
820
821
|
if (object.backendId && object.objectKey) {
|
|
821
|
-
const backend = await instance.backend(object.backendId), key = object.objectKey;
|
|
822
|
+
const backend = await instance.backend(object.backendId, tx), key = object.objectKey;
|
|
822
823
|
await transfer(backend.spec, (store) => store.remove(key));
|
|
823
824
|
}
|
|
824
825
|
await tx.delete(blobObjects).where(eq(blobObjects.id, object.id));
|
|
@@ -872,6 +873,14 @@ function imageResponse(blob) {
|
|
|
872
873
|
}
|
|
873
874
|
//#endregion
|
|
874
875
|
//#region src/adapters/elasticsearch.ts
|
|
876
|
+
const fields$1 = [
|
|
877
|
+
"title^3",
|
|
878
|
+
"key^3",
|
|
879
|
+
"description",
|
|
880
|
+
"acceptanceCriteria",
|
|
881
|
+
"labels",
|
|
882
|
+
"scopes"
|
|
883
|
+
];
|
|
875
884
|
function fail$1(_status, _code, message) {
|
|
876
885
|
throw new Error(message);
|
|
877
886
|
}
|
|
@@ -970,37 +979,44 @@ function createElasticsearchIndex(configuration) {
|
|
|
970
979
|
let scrollId;
|
|
971
980
|
try {
|
|
972
981
|
const found = [];
|
|
982
|
+
let score;
|
|
973
983
|
let response = await accepted(await call$1(settings, `${settings.index}/_search?scroll=1m`, {
|
|
974
984
|
method: "POST",
|
|
975
985
|
body: JSON.stringify({
|
|
976
986
|
size: 1e3,
|
|
977
987
|
_source: false,
|
|
978
|
-
sort: ["_doc"],
|
|
979
988
|
query: { bool: {
|
|
980
989
|
filter: [{ terms: { projectId: [...projectIds] } }, { term: { instanceId: identity } }],
|
|
981
|
-
must: [{ multi_match: {
|
|
982
|
-
query,
|
|
983
|
-
fields:
|
|
984
|
-
"title^3",
|
|
985
|
-
"key^3",
|
|
986
|
-
"description",
|
|
987
|
-
"acceptanceCriteria",
|
|
988
|
-
"labels",
|
|
989
|
-
"scopes"
|
|
990
|
-
],
|
|
990
|
+
must: [...query.terms ? [{ multi_match: {
|
|
991
|
+
query: query.terms,
|
|
992
|
+
fields: fields$1,
|
|
991
993
|
operator: "and",
|
|
992
994
|
fuzziness: "AUTO"
|
|
993
|
-
} }]
|
|
995
|
+
} }] : [], ...query.phrases.map((phrase) => ({ multi_match: {
|
|
996
|
+
query: phrase,
|
|
997
|
+
fields: fields$1,
|
|
998
|
+
type: "phrase"
|
|
999
|
+
} }))]
|
|
994
1000
|
} }
|
|
995
1001
|
})
|
|
996
1002
|
}));
|
|
997
1003
|
for (;;) {
|
|
998
1004
|
const result = z.object({
|
|
999
1005
|
_scroll_id: z.string().optional(),
|
|
1000
|
-
hits: z.object({ hits: z.array(z.object({
|
|
1006
|
+
hits: z.object({ hits: z.array(z.object({
|
|
1007
|
+
_id: z.uuid(),
|
|
1008
|
+
_score: z.number()
|
|
1009
|
+
})) })
|
|
1001
1010
|
}).parse(await response.json());
|
|
1002
1011
|
scrollId = result["_scroll_id"] ?? scrollId;
|
|
1003
|
-
|
|
1012
|
+
for (const hit of result.hits.hits) {
|
|
1013
|
+
const rank = (found.at(-1)?.rank ?? 0) + (score === hit["_score"] ? 0 : 1);
|
|
1014
|
+
score = hit["_score"];
|
|
1015
|
+
found.push({
|
|
1016
|
+
id: hit["_id"],
|
|
1017
|
+
rank
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1004
1020
|
if (found.length > 1e4) throw new Error("External search unavailable");
|
|
1005
1021
|
if (!result.hits.hits.length || !scrollId) break;
|
|
1006
1022
|
response = await accepted(await call$1(settings, "_search/scroll", {
|
|
@@ -1034,12 +1050,32 @@ const createTicketIndex = (configuration) => {
|
|
|
1034
1050
|
//#endregion
|
|
1035
1051
|
//#region src/search.ts
|
|
1036
1052
|
const likePattern = (query) => `%${query.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
|
|
1053
|
+
function parseQuery(query) {
|
|
1054
|
+
const phrases = [];
|
|
1055
|
+
const terms = query.replace(/"([^"]*)"/gu, (_, phrase) => {
|
|
1056
|
+
const value = phrase.replace(/\s+/gu, " ").trim();
|
|
1057
|
+
if (value && phrases.length < 8 && !phrases.some((seen) => seen.toLowerCase() === value.toLowerCase())) phrases.push(value);
|
|
1058
|
+
return " ";
|
|
1059
|
+
}).replace(/\s+/gu, " ").trim();
|
|
1060
|
+
return phrases.length || terms ? {
|
|
1061
|
+
phrases,
|
|
1062
|
+
terms
|
|
1063
|
+
} : {
|
|
1064
|
+
phrases,
|
|
1065
|
+
terms: query
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const querySegments = ({ phrases, terms }) => terms ? [...phrases, terms] : phrases;
|
|
1037
1069
|
function postgresSearch(query) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1070
|
+
return and(...querySegments(parseQuery(query)).map((segment) => {
|
|
1071
|
+
const value = likePattern(segment);
|
|
1072
|
+
return or(ilike(tickets.title, value), ilike(tickets.description, value), ilike(tickets.key, value), ilike(tickets.acceptanceCriteria, value), sql`array_to_string(${tickets.labels}, ' ') ILIKE ${value}`, sql`array_to_string(${tickets.scopes}, ' ') ILIKE ${value}`);
|
|
1073
|
+
}));
|
|
1040
1074
|
}
|
|
1075
|
+
const postgresRank = (query) => sql`CASE WHEN ${and(...querySegments(parseQuery(query)).map((segment) => ilike(tickets.title, likePattern(segment))))} THEN 0 ELSE 1 END`;
|
|
1041
1076
|
const builtinSearch = { where: async (_db, _ids, query) => ({
|
|
1042
1077
|
where: postgresSearch(query),
|
|
1078
|
+
rank: postgresRank(query),
|
|
1043
1079
|
engine: "postgres",
|
|
1044
1080
|
indexing: false
|
|
1045
1081
|
}) };
|
|
@@ -1093,6 +1129,7 @@ function createTicketSearch(database, instance, factory = createTicketIndex, sch
|
|
|
1093
1129
|
const settings = await instance.search();
|
|
1094
1130
|
const fallback = () => ({
|
|
1095
1131
|
where: postgresSearch(query),
|
|
1132
|
+
rank: postgresRank(query),
|
|
1096
1133
|
engine: "postgres",
|
|
1097
1134
|
indexing: !!settings
|
|
1098
1135
|
});
|
|
@@ -1101,12 +1138,14 @@ function createTicketSearch(database, instance, factory = createTicketIndex, sch
|
|
|
1101
1138
|
if (!state || !clock || state.error || state.configHash !== fingerprint(settings) || state.cursor !== clock.value) return fallback();
|
|
1102
1139
|
const root = await instance.read();
|
|
1103
1140
|
try {
|
|
1104
|
-
const
|
|
1141
|
+
const hits = await factory({
|
|
1105
1142
|
provider: "elasticsearch",
|
|
1106
1143
|
config: settings
|
|
1107
|
-
}).find(root.identity, projectIds, query);
|
|
1144
|
+
}).find(root.identity, projectIds, parseQuery(query));
|
|
1145
|
+
const ids = hits.map((hit) => hit.id), ranks = hits.map((hit) => hit.rank);
|
|
1108
1146
|
return {
|
|
1109
1147
|
where: ids.length ? inArray(tickets.id, ids) : sql`false`,
|
|
1148
|
+
rank: sql`(${`{${ranks.join(",")}}`}::int[])[array_position(${`{${ids.join(",")}}`}::uuid[], ${tickets.id})]`,
|
|
1110
1149
|
engine: "index",
|
|
1111
1150
|
indexing: false
|
|
1112
1151
|
};
|
|
@@ -1700,7 +1739,7 @@ async function rememberLabelColor(ctx, color) {
|
|
|
1700
1739
|
async function changeLabelUsage(ctx, name, replacement) {
|
|
1701
1740
|
const affected = await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, project(ctx).id), sql`${name} = ANY(${tickets.labels})`));
|
|
1702
1741
|
const pending = [];
|
|
1703
|
-
for (const ticket of affected) pending.push((await
|
|
1742
|
+
for (const ticket of affected) pending.push((await updateTicketLabels(ctx, {
|
|
1704
1743
|
ticketId: ticket.id,
|
|
1705
1744
|
expectedVersion: ticket.version,
|
|
1706
1745
|
labels: [...new Set(replacement ? ticket.labels.map((tag) => tag === name ? replacement : tag) : ticket.labels.filter((tag) => tag !== name))]
|
|
@@ -1997,7 +2036,7 @@ async function rewriteTicketValues(ctx, fieldId, map) {
|
|
|
1997
2036
|
if (value === void 0) continue;
|
|
1998
2037
|
const next = map(value);
|
|
1999
2038
|
if (JSON.stringify(next) === JSON.stringify(value)) continue;
|
|
2000
|
-
await
|
|
2039
|
+
await assertDefinitionMigration(ctx, [ticket]);
|
|
2001
2040
|
const values = { ...ticket.customFields };
|
|
2002
2041
|
if (next === null) delete values[fieldId];
|
|
2003
2042
|
else values[fieldId] = next;
|
|
@@ -2254,23 +2293,17 @@ async function validateReferences(ctx, ticketId, changes) {
|
|
|
2254
2293
|
}
|
|
2255
2294
|
async function assertNoCycle(ctx, sourceId, targetId) {
|
|
2256
2295
|
if (sourceId === targetId) fail(409, "dependency_cycle", "A ticket cannot depend on itself");
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
const node = pending.pop();
|
|
2269
|
-
if (!node || visited.has(node)) continue;
|
|
2270
|
-
if (node === sourceId) fail(409, "dependency_cycle", "This relationship would create a dependency cycle");
|
|
2271
|
-
visited.add(node);
|
|
2272
|
-
pending.push(...edges.get(node) ?? []);
|
|
2273
|
-
}
|
|
2296
|
+
const { rows } = await ctx.db.execute(sql`WITH RECURSIVE reach(id) AS (
|
|
2297
|
+
SELECT ${targetId}::uuid
|
|
2298
|
+
UNION
|
|
2299
|
+
SELECT edge.next FROM reach JOIN (
|
|
2300
|
+
SELECT source_id AS id, target_id AS next FROM ${relations$1} WHERE type = 'blocks'
|
|
2301
|
+
UNION ALL
|
|
2302
|
+
SELECT id, parent_id FROM ${tickets} WHERE parent_id IS NOT NULL
|
|
2303
|
+
) edge ON edge.id = reach.id
|
|
2304
|
+
)
|
|
2305
|
+
SELECT 1 FROM reach WHERE id = ${sourceId}::uuid LIMIT 1`);
|
|
2306
|
+
if (rows.length) fail(409, "dependency_cycle", "This relationship would create a dependency cycle");
|
|
2274
2307
|
}
|
|
2275
2308
|
async function updateTicketRow(ctx, input) {
|
|
2276
2309
|
const current = await getTicket(ctx, input.ticketId);
|
|
@@ -2278,12 +2311,18 @@ async function updateTicketRow(ctx, input) {
|
|
|
2278
2311
|
await assertWorkerClaim(ctx, current, input.claimId);
|
|
2279
2312
|
return persistTicketChanges(ctx, current, input.changes);
|
|
2280
2313
|
}
|
|
2281
|
-
async function
|
|
2314
|
+
async function updateTicketLabels(ctx, input) {
|
|
2282
2315
|
if (!can(ctx, "project.manage") && !can(ctx, "workspace.manage")) fail(403, "forbidden", "Label administration permission is required");
|
|
2283
2316
|
const current = await getTicket(ctx, input.ticketId);
|
|
2284
2317
|
assertVersion(current, input.expectedVersion, "ticket");
|
|
2285
2318
|
return persistTicketChanges(ctx, current, { labels: input.labels });
|
|
2286
2319
|
}
|
|
2320
|
+
async function migrateTicketRow(ctx, input) {
|
|
2321
|
+
const current = await getTicket(ctx, input.ticketId);
|
|
2322
|
+
assertVersion(current, input.expectedVersion, "ticket");
|
|
2323
|
+
await assertDefinitionMigration(ctx, [current]);
|
|
2324
|
+
return persistTicketChanges(ctx, current, input.changes);
|
|
2325
|
+
}
|
|
2287
2326
|
async function persistTicketChanges(ctx, current, changes) {
|
|
2288
2327
|
await validateReferences(ctx, current.id, changes);
|
|
2289
2328
|
const state = await resolveState(ctx, changes, current);
|
|
@@ -2411,7 +2450,7 @@ async function createTicketRow(ctx, input) {
|
|
|
2411
2450
|
...fields,
|
|
2412
2451
|
...state,
|
|
2413
2452
|
...dates,
|
|
2414
|
-
customFields: applyCustomFieldValues(await projectCustomFields(ctx), null, customFields),
|
|
2453
|
+
customFields: intakeOnly(ctx.actor) ? {} : applyCustomFieldValues(await projectCustomFields(ctx), null, customFields),
|
|
2415
2454
|
...await allocateTicket(ctx),
|
|
2416
2455
|
id,
|
|
2417
2456
|
createdBy: ctx.actor.id
|
|
@@ -2458,6 +2497,20 @@ const hitRow = z.object({
|
|
|
2458
2497
|
title: z.string().nullable(),
|
|
2459
2498
|
status: z.enum(states).nullable()
|
|
2460
2499
|
});
|
|
2500
|
+
const summarize = (ticket) => ticketSummarySchema.parse(json(ticket));
|
|
2501
|
+
const snippetLength = 240;
|
|
2502
|
+
function snippet(body, query) {
|
|
2503
|
+
const text = body?.replace(/\s+/gu, " ").trim() ?? "", lower = text.toLowerCase(), parsed = parseQuery(query);
|
|
2504
|
+
let at = -1, size = 0;
|
|
2505
|
+
for (const needle of [...querySegments(parsed), ...parsed.terms.split(" ")]) {
|
|
2506
|
+
const found = needle ? lower.indexOf(needle.toLowerCase()) : -1;
|
|
2507
|
+
if (found >= 0 && (at < 0 || found < at)) [at, size] = [found, needle.length];
|
|
2508
|
+
}
|
|
2509
|
+
if (at < 0) return "";
|
|
2510
|
+
if (text.length <= snippetLength) return text;
|
|
2511
|
+
const start = Math.max(0, Math.min(at + Math.floor((size - snippetLength) / 2), text.length - snippetLength)), end = start + snippetLength;
|
|
2512
|
+
return `${start > 0 ? "…" : ""}${text.slice(start > 0 ? start + 1 : start, end < text.length ? end - 1 : end)}${end < text.length ? "…" : ""}`;
|
|
2513
|
+
}
|
|
2461
2514
|
function excerpt(body, query, radius = 80) {
|
|
2462
2515
|
const text = body?.replace(/\s+/gu, " ").trim() ?? "", at = text.toLowerCase().indexOf(query.toLowerCase());
|
|
2463
2516
|
if (at < 0) return "";
|
|
@@ -2475,25 +2528,29 @@ async function deleteTicketRows(ctx, selection) {
|
|
|
2475
2528
|
const ids = [...selected.keys()];
|
|
2476
2529
|
const [claimed] = await ctx.db.select({ ticketId: claims.ticketId }).from(claims).where(inArray(claims.ticketId, ids)).limit(1);
|
|
2477
2530
|
if (claimed) fail(409, "claim_conflict", "Release ownership before deleting this ticket", { ticketId: claimed.ticketId });
|
|
2478
|
-
await publishMany(ctx, [
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2531
|
+
await publishMany(ctx, [
|
|
2532
|
+
...(await ctx.db.update(tickets).set({
|
|
2533
|
+
parentId: null,
|
|
2534
|
+
version: sql`${tickets.version} + 1`,
|
|
2535
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2536
|
+
}).where(and(inArray(tickets.parentId, ids), notInArray(tickets.id, ids))).returning()).map((child) => ({
|
|
2537
|
+
type: "ticket.updated",
|
|
2538
|
+
ticketId: child.id,
|
|
2539
|
+
data: {
|
|
2540
|
+
version: child.version,
|
|
2541
|
+
changedFields: ["parentId"]
|
|
2542
|
+
}
|
|
2543
|
+
})),
|
|
2544
|
+
...[...selected.values()].map((ticket) => ({
|
|
2545
|
+
type: "ticket.deleted",
|
|
2546
|
+
ticketId: ticket.id,
|
|
2547
|
+
data: {
|
|
2548
|
+
key: ticket.key,
|
|
2549
|
+
version: ticket.version
|
|
2550
|
+
}
|
|
2551
|
+
})),
|
|
2552
|
+
...await severedRelationEvents(ctx, ids)
|
|
2553
|
+
]);
|
|
2497
2554
|
await ctx.db.delete(tickets).where(inArray(tickets.id, ids));
|
|
2498
2555
|
}
|
|
2499
2556
|
async function relocation(ctx, ticket, destinationProjectId) {
|
|
@@ -2511,6 +2568,10 @@ async function relocation(ctx, ticket, destinationProjectId) {
|
|
|
2511
2568
|
code: "claimed",
|
|
2512
2569
|
message: `Claimed by ${claim.workerId}; release or recover the claim first`
|
|
2513
2570
|
});
|
|
2571
|
+
else if (ctx.actor.machine && ticket.claimRequired) blockers.push({
|
|
2572
|
+
code: "claim_conflict",
|
|
2573
|
+
message: "A worker needs a claim to move this ticket once it has been claimed; a person can move it, or return it to To do so a worker can claim it again"
|
|
2574
|
+
});
|
|
2514
2575
|
const [parent] = ticket.parentId ? await ctx.db.select({
|
|
2515
2576
|
id: tickets.id,
|
|
2516
2577
|
key: tickets.key
|
|
@@ -2558,6 +2619,16 @@ async function relocation(ctx, ticket, destinationProjectId) {
|
|
|
2558
2619
|
fields
|
|
2559
2620
|
};
|
|
2560
2621
|
}
|
|
2622
|
+
async function readableTicket(ctx, identifier) {
|
|
2623
|
+
const allowed = await accessibleProjectRows(ctx.db, ctx.actor);
|
|
2624
|
+
const [ticket] = allowed.length ? await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, allowed.map((row) => row.id)), ticketMatch(identifier))).limit(1) : [];
|
|
2625
|
+
const owner = allowed.find((row) => row.id === ticket?.projectId);
|
|
2626
|
+
if (!ticket || !owner) fail(404, "not_found", "Ticket was not found");
|
|
2627
|
+
return {
|
|
2628
|
+
ticket,
|
|
2629
|
+
project: owner
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2561
2632
|
const ticketHandlers = {
|
|
2562
2633
|
async list_ticket_labels(ctx, input) {
|
|
2563
2634
|
const labels = labelUsage(ctx, input.archived).as("usage");
|
|
@@ -2568,7 +2639,11 @@ const ticketHandlers = {
|
|
|
2568
2639
|
const related = input.relatedTo ? await getTicket(ctx, input.relatedTo) : void 0;
|
|
2569
2640
|
const search = input.q ? await ctx.search.where(ctx.db, [project(ctx).id], input.q) : void 0;
|
|
2570
2641
|
await assertCustomFieldFilters(ctx, [project(ctx).id], input);
|
|
2571
|
-
|
|
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?.where), ticketOrder(input), input);
|
|
2643
|
+
return input.view === "summary" ? {
|
|
2644
|
+
...page,
|
|
2645
|
+
items: page.items.map(summarize)
|
|
2646
|
+
} : page;
|
|
2572
2647
|
},
|
|
2573
2648
|
async list_my_work(ctx, input) {
|
|
2574
2649
|
const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
|
|
@@ -2597,6 +2672,21 @@ const ticketHandlers = {
|
|
|
2597
2672
|
};
|
|
2598
2673
|
},
|
|
2599
2674
|
async create_ticket(ctx, input) {
|
|
2675
|
+
if (intakeOnly(ctx.actor)) input = {
|
|
2676
|
+
...input,
|
|
2677
|
+
automationReady: false,
|
|
2678
|
+
holdReason: intakeHoldReason,
|
|
2679
|
+
status: "todo",
|
|
2680
|
+
stateId: void 0,
|
|
2681
|
+
assigneeId: null,
|
|
2682
|
+
parentId: null,
|
|
2683
|
+
archived: false,
|
|
2684
|
+
dueAt: null,
|
|
2685
|
+
startAt: null,
|
|
2686
|
+
customFields: {},
|
|
2687
|
+
uploads: [],
|
|
2688
|
+
labels: operations.create_ticket.input.shape.labels.parse([.../* @__PURE__ */ new Set([...input.labels, "intake"])])
|
|
2689
|
+
};
|
|
2600
2690
|
const { row, event } = await createTicketRow(ctx, input);
|
|
2601
2691
|
await publishMany(ctx, [event, ...(await claimStagedUploads(ctx, row.id, input.uploads)).map((item) => ({
|
|
2602
2692
|
type: "attachment.created",
|
|
@@ -2642,10 +2732,15 @@ const ticketHandlers = {
|
|
|
2642
2732
|
...event.data,
|
|
2643
2733
|
duplicateOf: source.id
|
|
2644
2734
|
}
|
|
2645
|
-
}, ...relationEvents("relation.added", relation)]);
|
|
2735
|
+
}, ...relationEvents("relation.added", relation, [row, source])]);
|
|
2646
2736
|
return row;
|
|
2647
2737
|
},
|
|
2648
2738
|
update_ticket: updateTicket,
|
|
2739
|
+
async update_ticket_labels(ctx, input) {
|
|
2740
|
+
const { row, event } = await updateTicketLabels(ctx, input);
|
|
2741
|
+
await publish(ctx, event.type, event.ticketId, event.data);
|
|
2742
|
+
return row;
|
|
2743
|
+
},
|
|
2649
2744
|
async move_ticket(ctx, input) {
|
|
2650
2745
|
const current = await getTicket(ctx, input.ticketId);
|
|
2651
2746
|
assertVersion(current, input.expectedVersion, "ticket", "moving");
|
|
@@ -2686,7 +2781,6 @@ const ticketHandlers = {
|
|
|
2686
2781
|
assertVersion(current, input.expectedVersion, "ticket", "moving");
|
|
2687
2782
|
const { target, blockers, fields } = await relocation(ctx, current, input.destinationProjectId);
|
|
2688
2783
|
if (blockers.length) fail(409, "relocation_blocked", blockers.map((item) => item.message).join(". "), { blockers });
|
|
2689
|
-
await assertWorkerClaim(ctx, current);
|
|
2690
2784
|
const state = requireRow(await resolveState(target, {
|
|
2691
2785
|
status: input.status ?? (input.stateId ? void 0 : z.enum(states).parse(current.status)),
|
|
2692
2786
|
stateId: input.stateId
|
|
@@ -2728,10 +2822,7 @@ const ticketHandlers = {
|
|
|
2728
2822
|
return row;
|
|
2729
2823
|
},
|
|
2730
2824
|
async resolve_ticket(ctx, input) {
|
|
2731
|
-
const
|
|
2732
|
-
const [ticket] = allowed.length ? await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, allowed.map((row) => row.id)), ticketMatch(input.ticketId))).limit(1) : [];
|
|
2733
|
-
const owner = allowed.find((row) => row.id === ticket?.projectId);
|
|
2734
|
-
if (!ticket || !owner) fail(404, "not_found", "Ticket was not found");
|
|
2825
|
+
const { ticket, project: owner } = await readableTicket(ctx, input.ticketId);
|
|
2735
2826
|
return {
|
|
2736
2827
|
ticket,
|
|
2737
2828
|
project: {
|
|
@@ -2748,6 +2839,7 @@ const ticketHandlers = {
|
|
|
2748
2839
|
projectId: input.projectId,
|
|
2749
2840
|
ticketId: item.ticketId,
|
|
2750
2841
|
expectedVersion: item.expectedVersion,
|
|
2842
|
+
claimId: item.claimId,
|
|
2751
2843
|
changes: input.changes
|
|
2752
2844
|
});
|
|
2753
2845
|
result.push(row);
|
|
@@ -2778,17 +2870,20 @@ const ticketHandlers = {
|
|
|
2778
2870
|
async search_tickets(ctx, input) {
|
|
2779
2871
|
const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
|
|
2780
2872
|
if (!allowed.length) return { items: [] };
|
|
2781
|
-
const ids = allowed.map((row) => row.id);
|
|
2782
|
-
return { items: (await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, ids), eq(tickets.archived, false), or(ticketMatch(input.q),
|
|
2873
|
+
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(sql`CASE WHEN ${ticketMatch(input.q)} THEN 0 ELSE 1 END`, sql`${match.rank} NULLS LAST`, desc(tickets.updatedAt)).limit(input.limit)).flatMap((ticket) => {
|
|
2783
2875
|
const owner = allowed.find((row) => row.id === ticket.projectId);
|
|
2784
|
-
|
|
2785
|
-
|
|
2876
|
+
if (!owner) return [];
|
|
2877
|
+
const summary = input.view === "summary", body = `${ticket.description} ${ticket.acceptanceCriteria}`;
|
|
2878
|
+
return [{
|
|
2879
|
+
ticket: summary ? summarize(ticket) : ticket,
|
|
2786
2880
|
project: {
|
|
2787
2881
|
id: owner.id,
|
|
2788
2882
|
key: owner.key,
|
|
2789
2883
|
name: owner.name
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2884
|
+
},
|
|
2885
|
+
...summary && { snippet: snippet(body, input.q) }
|
|
2886
|
+
}];
|
|
2792
2887
|
}) };
|
|
2793
2888
|
},
|
|
2794
2889
|
async search_workspace(ctx, input) {
|
|
@@ -2813,23 +2908,26 @@ const ticketHandlers = {
|
|
|
2813
2908
|
filtered ? void 0 : sql`SELECT 'project' AS type, projects.id::text AS id, projects.id AS project_id,
|
|
2814
2909
|
NULL::uuid AS ticket_id, NULL::text AS actor_id, projects.description AS body,
|
|
2815
2910
|
NULL::text AS event_type, NULL::jsonb AS data, projects.updated_at AS at,
|
|
2816
|
-
CASE WHEN projects.key = ${key} THEN 0 ELSE 1 END AS rank, 0 AS family
|
|
2911
|
+
CASE WHEN projects.key = ${key} THEN 0 ELSE 1 END AS rank, 0 AS family,
|
|
2912
|
+
NULL::int AS score
|
|
2817
2913
|
FROM projects WHERE ${inArray(projects.id, ids)}
|
|
2818
2914
|
AND (projects.name ILIKE ${like} OR projects.key ILIKE ${like} OR projects.description ILIKE ${like})`,
|
|
2819
2915
|
sql`SELECT 'ticket' AS type, tickets.id::text AS id, tickets.project_id,
|
|
2820
2916
|
tickets.id AS ticket_id, NULL::text AS actor_id,
|
|
2821
2917
|
concat_ws(' ', tickets.description, tickets.acceptance_criteria) AS body,
|
|
2822
2918
|
NULL::text AS event_type, NULL::jsonb AS data, tickets.updated_at AS at,
|
|
2823
|
-
CASE WHEN ${ticketMatch(input.q)} THEN 0 ELSE 1 END AS rank, 1 AS family
|
|
2919
|
+
CASE WHEN ${ticketMatch(input.q)} THEN 0 ELSE 1 END AS rank, 1 AS family,
|
|
2920
|
+
${match.rank}::int AS score
|
|
2824
2921
|
FROM tickets WHERE ${and(ticketScope, or(ticketMatch(input.q), match.where))}`,
|
|
2825
2922
|
sql`SELECT 'comment' AS type, comments.id::text AS id, comments.project_id,
|
|
2826
2923
|
comments.ticket_id, comments.actor_id, comments.body, NULL::text AS event_type,
|
|
2827
|
-
NULL::jsonb AS data, comments.created_at AS at, 1 AS rank, 2 AS family
|
|
2924
|
+
NULL::jsonb AS data, comments.created_at AS at, 1 AS rank, 2 AS family,
|
|
2925
|
+
NULL::int AS score
|
|
2828
2926
|
FROM comments JOIN tickets ON tickets.id = comments.ticket_id
|
|
2829
2927
|
WHERE ${and(ticketScope, sql`comments.body ILIKE ${like}`)}`,
|
|
2830
2928
|
sql`SELECT 'activity' AS type, events.id::text AS id, events.project_id,
|
|
2831
2929
|
events.ticket_id, events.actor_id, NULL::text AS body, events.type AS event_type,
|
|
2832
|
-
events.data, events.created_at AS at, 1 AS rank, 3 AS family
|
|
2930
|
+
events.data, events.created_at AS at, 1 AS rank, 3 AS family, NULL::int AS score
|
|
2833
2931
|
FROM events
|
|
2834
2932
|
LEFT JOIN tickets ON tickets.id = events.ticket_id AND tickets.project_id = events.project_id
|
|
2835
2933
|
LEFT JOIN "user" ON "user".id = events.actor_id
|
|
@@ -2846,14 +2944,15 @@ const ticketHandlers = {
|
|
|
2846
2944
|
FROM (${hits}) AS hits
|
|
2847
2945
|
LEFT JOIN tickets ON tickets.id = hits.ticket_id AND tickets.project_id = hits.project_id
|
|
2848
2946
|
WHERE ${input.type ? sql`hits.type = ${input.type}` : sql`true`}
|
|
2849
|
-
ORDER BY hits.rank, hits.family, hits.at DESC, hits.id DESC
|
|
2947
|
+
ORDER BY hits.rank, hits.family, hits.score NULLS LAST, hits.at DESC, hits.id DESC
|
|
2850
2948
|
LIMIT ${input.limit} OFFSET ${input.offset}`)).rows.map((row) => hitRow.parse(row));
|
|
2851
2949
|
const projectsById = new Map(allowed.map((row) => [row.id, row]));
|
|
2950
|
+
const ticketIds = rows.flatMap((row) => row.ticket_id ? [row.ticket_id] : []), summaries = input.view === "summary" && ticketIds.length ? new Map((await ctx.db.select().from(tickets).where(inArray(tickets.id, ticketIds))).map((row) => [row.id, summarize(row)])) : void 0;
|
|
2852
2951
|
return {
|
|
2853
2952
|
items: rows.flatMap((row) => {
|
|
2854
2953
|
const owner = projectsById.get(row.project_id);
|
|
2855
2954
|
if (!owner) return [];
|
|
2856
|
-
const ticket = row.ticket_id && row.key && row.title && row.status ? {
|
|
2955
|
+
const ticket = row.ticket_id && row.key && row.title && row.status ? summaries?.get(row.ticket_id) ?? {
|
|
2857
2956
|
id: row.ticket_id,
|
|
2858
2957
|
key: row.key,
|
|
2859
2958
|
title: row.title,
|
|
@@ -2879,6 +2978,7 @@ const ticketHandlers = {
|
|
|
2879
2978
|
createdAt: at
|
|
2880
2979
|
}, true) : ticket?.title ?? "",
|
|
2881
2980
|
excerpt: excerpt(row.body, input.q),
|
|
2981
|
+
...input.view === "summary" && { snippet: snippet(row.body, input.q) },
|
|
2882
2982
|
actorId: row.actor_id,
|
|
2883
2983
|
at
|
|
2884
2984
|
}];
|
|
@@ -2950,6 +3050,16 @@ const ticketHandlers = {
|
|
|
2950
3050
|
version: row.version,
|
|
2951
3051
|
changedFields: ["status", "stateId"]
|
|
2952
3052
|
});
|
|
3053
|
+
} else if (ticket.claimRequired) {
|
|
3054
|
+
const row = one(await ctx.db.update(tickets).set({
|
|
3055
|
+
claimRequired: false,
|
|
3056
|
+
version: ticket.version + 1,
|
|
3057
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3058
|
+
}).where(eq(tickets.id, ticket.id)).returning());
|
|
3059
|
+
await publish(ctx, "ticket.updated", ticket.id, {
|
|
3060
|
+
version: row.version,
|
|
3061
|
+
changedFields: ["claimRequired"]
|
|
3062
|
+
});
|
|
2953
3063
|
}
|
|
2954
3064
|
await ctx.db.delete(claims).where(eq(claims.id, current.id));
|
|
2955
3065
|
await publish(ctx, "claim.recovered", ticket.id, {
|
|
@@ -3234,6 +3344,13 @@ function mentionedUserIds(markdown) {
|
|
|
3234
3344
|
//#region src/collaboration-operations.ts
|
|
3235
3345
|
const sourceTicket = alias(tickets, "source_ticket");
|
|
3236
3346
|
const targetTicket = alias(tickets, "target_ticket");
|
|
3347
|
+
const sourceProject = alias(projects, "source_project");
|
|
3348
|
+
const targetProject = alias(projects, "target_project");
|
|
3349
|
+
async function assertMayConstrain(ctx, relation, sides) {
|
|
3350
|
+
const constrained = relation.type === "blocks" ? relation.targetId : relation.type === "duplicate" ? relation.sourceId : null;
|
|
3351
|
+
const side = sides.find((item) => item.id === constrained);
|
|
3352
|
+
if (side && side.projectId !== project(ctx).id) await context(ctx.db, ctx.actor, "write", side.projectId);
|
|
3353
|
+
}
|
|
3237
3354
|
async function editableComment(ctx, commentId, expectedVersion) {
|
|
3238
3355
|
const row = one(await ctx.db.select().from(comments).where(and(eq(comments.projectId, project(ctx).id), eq(comments.id, commentId))));
|
|
3239
3356
|
if (row.actorId !== ctx.actor.id && !can(ctx, "project.manage")) fail(403, "forbidden", "Only the author or a project administrator can edit this comment");
|
|
@@ -3264,18 +3381,45 @@ async function addComment(ctx, input) {
|
|
|
3264
3381
|
const collaborationHandlers = {
|
|
3265
3382
|
async list_relations(ctx, input) {
|
|
3266
3383
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
3267
|
-
|
|
3384
|
+
const rows = await ctx.db.select({
|
|
3268
3385
|
...getTableColumns(relations$1),
|
|
3269
3386
|
sourceKey: sourceTicket.key,
|
|
3270
3387
|
sourceTitle: sourceTicket.title,
|
|
3388
|
+
sourceProjectId: sourceTicket.projectId,
|
|
3389
|
+
sourceProjectKey: sourceProject.key,
|
|
3271
3390
|
targetKey: targetTicket.key,
|
|
3272
|
-
targetTitle: targetTicket.title
|
|
3273
|
-
|
|
3391
|
+
targetTitle: targetTicket.title,
|
|
3392
|
+
targetProjectId: targetTicket.projectId,
|
|
3393
|
+
targetProjectKey: targetProject.key
|
|
3394
|
+
}).from(relations$1).innerJoin(sourceTicket, eq(sourceTicket.id, relations$1.sourceId)).innerJoin(targetTicket, eq(targetTicket.id, relations$1.targetId)).innerJoin(sourceProject, eq(sourceProject.id, sourceTicket.projectId)).innerJoin(targetProject, eq(targetProject.id, targetTicket.projectId)).where(or(eq(relations$1.sourceId, ticket.id), eq(relations$1.targetId, ticket.id)));
|
|
3395
|
+
const here = project(ctx).id, elsewhere = rows.some((row) => row.sourceProjectId !== here || row.targetProjectId !== here), readable = /* @__PURE__ */ new Set([here, ...(elsewhere ? await accessibleProjectRows(ctx.db, ctx.actor) : []).map((row) => row.id)]);
|
|
3396
|
+
return rows.map(({ sourceTitle, sourceProjectId, sourceProjectKey, targetTitle, targetProjectId, targetProjectKey, ...row }) => {
|
|
3397
|
+
const source = readable.has(sourceProjectId), target = readable.has(targetProjectId);
|
|
3398
|
+
return {
|
|
3399
|
+
...row,
|
|
3400
|
+
...source && {
|
|
3401
|
+
sourceTitle,
|
|
3402
|
+
sourceProjectId,
|
|
3403
|
+
sourceProjectKey
|
|
3404
|
+
},
|
|
3405
|
+
...target && {
|
|
3406
|
+
targetTitle,
|
|
3407
|
+
targetProjectId,
|
|
3408
|
+
targetProjectKey
|
|
3409
|
+
},
|
|
3410
|
+
restricted: !source || !target
|
|
3411
|
+
};
|
|
3412
|
+
});
|
|
3274
3413
|
},
|
|
3275
3414
|
async add_relation(ctx, input) {
|
|
3276
|
-
const current = await getTicket(ctx, input.ticketId), other = await
|
|
3415
|
+
const current = await getTicket(ctx, input.ticketId), { ticket: other } = await readableTicket(ctx, input.targetId);
|
|
3277
3416
|
const [source, target] = input.direction === "incoming" ? [other, current] : [current, other];
|
|
3278
3417
|
if (source.id === target.id) fail(409, "invalid_relation", "Choose a different ticket");
|
|
3418
|
+
await assertMayConstrain(ctx, {
|
|
3419
|
+
type: input.type,
|
|
3420
|
+
sourceId: source.id,
|
|
3421
|
+
targetId: target.id
|
|
3422
|
+
}, [current, other]);
|
|
3279
3423
|
if (input.type === "blocks") await assertNoCycle(ctx, source.id, target.id);
|
|
3280
3424
|
const [sourceId, targetId] = input.type === "related" ? [source.id, target.id].toSorted() : [source.id, target.id];
|
|
3281
3425
|
const row = one(await ctx.db.insert(relations$1).values({
|
|
@@ -3291,12 +3435,19 @@ const collaborationHandlers = {
|
|
|
3291
3435
|
],
|
|
3292
3436
|
set: { type: input.type }
|
|
3293
3437
|
}).returning());
|
|
3294
|
-
await publishMany(ctx, relationEvents("relation.added", row));
|
|
3438
|
+
await publishMany(ctx, relationEvents("relation.added", row, [current, other]));
|
|
3295
3439
|
return row;
|
|
3296
3440
|
},
|
|
3297
3441
|
async remove_relation(ctx, input) {
|
|
3298
|
-
const
|
|
3299
|
-
await
|
|
3442
|
+
const found = one(await ctx.db.select().from(relations$1).where(eq(relations$1.id, input.relationId)));
|
|
3443
|
+
const sides = await ctx.db.select({
|
|
3444
|
+
id: tickets.id,
|
|
3445
|
+
projectId: tickets.projectId
|
|
3446
|
+
}).from(tickets).where(inArray(tickets.id, [found.sourceId, found.targetId]));
|
|
3447
|
+
if (!sides.some((side) => side.projectId === project(ctx).id)) fail(404, "not_found", "The requested record was not found");
|
|
3448
|
+
await assertMayConstrain(ctx, found, sides);
|
|
3449
|
+
const row = one(await ctx.db.delete(relations$1).where(eq(relations$1.id, found.id)).returning());
|
|
3450
|
+
await publishMany(ctx, relationEvents("relation.removed", row, sides));
|
|
3300
3451
|
return { ok: true };
|
|
3301
3452
|
},
|
|
3302
3453
|
async list_comments(ctx, input) {
|
|
@@ -3949,7 +4100,7 @@ function matches(rule, names, labels) {
|
|
|
3949
4100
|
const has = (id) => carried.has(names.get(id)?.toLowerCase() ?? "\0");
|
|
3950
4101
|
return rule.match === "all" ? rule.labels.every(has) : rule.labels.some(has);
|
|
3951
4102
|
}
|
|
3952
|
-
const qualifies = (rules, names, link, ticket) => link
|
|
4103
|
+
const qualifies = (rules, names, link, ticket) => !intakeQuarantined(ticket) && (link?.origin === "inbound" ? matches(rules.inbound, names, link.remote?.labels ?? []) : matches(rules.outbound, names, ticket.labels));
|
|
3953
4104
|
const workspaceOf = async (db, projectId) => one(await db.select({ workspaceId: projects.workspaceId }).from(projects).where(eq(projects.id, projectId))).workspaceId;
|
|
3954
4105
|
async function setLinks(db, projectId, links, changes) {
|
|
3955
4106
|
if (!links.length) return [];
|
|
@@ -4011,7 +4162,7 @@ async function queueIssueSync(db, rows) {
|
|
|
4011
4162
|
for (const ticket of found) for (const connection of connections) {
|
|
4012
4163
|
const link = links.find((item) => item.connectionId === connection.id && item.ticketId === ticket.id), labelled = names.get(connection.id) ?? /* @__PURE__ */ new Map();
|
|
4013
4164
|
if (!link) {
|
|
4014
|
-
if (connection.projectId === ticket.projectId && !ticket.archived && !complete(ticket.status) &&
|
|
4165
|
+
if (connection.projectId === ticket.projectId && !ticket.archived && !complete(ticket.status) && qualifies(connection, labelled, null, ticket)) await queueSync(db, await db.insert(issueLinks).values({
|
|
4015
4166
|
connectionId: connection.id,
|
|
4016
4167
|
ticketId: ticket.id,
|
|
4017
4168
|
origin: "outbound",
|
|
@@ -4032,8 +4183,9 @@ async function queueIssueSync(db, rows) {
|
|
|
4032
4183
|
state: "review",
|
|
4033
4184
|
reason: "labels"
|
|
4034
4185
|
});
|
|
4035
|
-
else if (link.state === "active" && !connection.paused) {
|
|
4186
|
+
else if ((link.state === "active" || link.state === "creating") && qualified && !connection.paused) {
|
|
4036
4187
|
await queueSync(db, [link]);
|
|
4188
|
+
if (link.state !== "active") continue;
|
|
4037
4189
|
const fresh = commentEvents.filter((event) => event.ticketId === ticket.id && !copied.has(`${link.id}:${String(event.data.commentId)}`));
|
|
4038
4190
|
if (fresh.length) await db.insert(deliveries).values(fresh.map((event) => ({
|
|
4039
4191
|
connectionId: connection.id,
|
|
@@ -4286,6 +4438,10 @@ async function sendIssueSync(database, senders, secrets, drivers, job, event) {
|
|
|
4286
4438
|
}).from(issueLinks).innerJoin(tickets, eq(tickets.id, issueLinks.ticketId)).innerJoin(repositoryConnections, eq(repositoryConnections.id, issueLinks.connectionId)).innerJoin(projects, eq(projects.id, repositoryConnections.projectId)).where(eq(issueLinks.id, job.issueLinkId ?? "")))[0];
|
|
4287
4439
|
if (!loaded) return finish(database.db, job, "link_removed");
|
|
4288
4440
|
const { link, ticket, connection } = loaded;
|
|
4441
|
+
if (intakeQuarantined(ticket)) return locked(database, async (db) => {
|
|
4442
|
+
const current = (await db.select({ holdReason: tickets.holdReason }).from(tickets).where(eq(tickets.id, ticket.id)))[0];
|
|
4443
|
+
if (!current || intakeQuarantined(current)) await finish(db, job, current ? "quarantined" : "link_removed");
|
|
4444
|
+
});
|
|
4289
4445
|
const skip = connection.paused ? "paused" : !loaded.writer ? "access_revoked" : loaded.archivedAt ? "project_archived" : ticket.projectId !== connection.projectId ? "moved" : null;
|
|
4290
4446
|
if (skip === "access_revoked" || skip === "project_archived") return locked(database, async (db) => {
|
|
4291
4447
|
await finish(db, job, skip);
|
|
@@ -4500,7 +4656,7 @@ async function plan(ctx, connection, { inbound, outbound }) {
|
|
|
4500
4656
|
if (link.state === "active") (qualified ? matching : pause).push(link);
|
|
4501
4657
|
else if (link.state === "paused" && link.reason === "labels" && qualified) review.push(link);
|
|
4502
4658
|
}
|
|
4503
|
-
const exported = rules.outbound.enabled ? (await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, connection.projectId), eq(tickets.archived, false), notInArray(tickets.status, [...closedStates]), notInArray(tickets.id, ctx.db.select({ id: issueLinks.ticketId }).from(issueLinks).where(eq(issueLinks.connectionId, connection.id)))))).filter((ticket) =>
|
|
4659
|
+
const exported = rules.outbound.enabled ? (await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, connection.projectId), eq(tickets.archived, false), notInArray(tickets.status, [...closedStates]), notInArray(tickets.id, ctx.db.select({ id: issueLinks.ticketId }).from(issueLinks).where(eq(issueLinks.connectionId, connection.id)))))).filter((ticket) => qualifies(rules, names, null, ticket)) : [];
|
|
4504
4660
|
return {
|
|
4505
4661
|
matching,
|
|
4506
4662
|
pause,
|
|
@@ -5063,7 +5219,7 @@ const workflowHandlers = {
|
|
|
5063
5219
|
if (moving) assertNotLast(all, current, "move");
|
|
5064
5220
|
const siblings = all.filter((state) => state.category === next && state.id !== current.id), at = Math.min(input.position ?? (moving ? siblings.length : current.position), siblings.length);
|
|
5065
5221
|
const held = moving ? await ctx.db.select().from(tickets).where(eq(tickets.stateId, current.id)) : [];
|
|
5066
|
-
|
|
5222
|
+
await assertDefinitionMigration(ctx, held);
|
|
5067
5223
|
const row = one(await ctx.db.update(workflowStates).set({
|
|
5068
5224
|
...input.name === void 0 ? {} : { name: input.name },
|
|
5069
5225
|
...input.color === void 0 ? {} : { color: input.color },
|
|
@@ -5101,8 +5257,7 @@ const workflowHandlers = {
|
|
|
5101
5257
|
assertNotLast(all, current, "delete");
|
|
5102
5258
|
const held = await ctx.db.select().from(tickets).where(eq(tickets.stateId, current.id));
|
|
5103
5259
|
const pending = [];
|
|
5104
|
-
for (const ticket of held) pending.push((await
|
|
5105
|
-
projectId: project(ctx).id,
|
|
5260
|
+
for (const ticket of held) pending.push((await migrateTicketRow(ctx, {
|
|
5106
5261
|
ticketId: ticket.id,
|
|
5107
5262
|
expectedVersion: ticket.version,
|
|
5108
5263
|
changes: { stateId: target.id }
|
|
@@ -5745,22 +5900,26 @@ async function removeProject(ctx) {
|
|
|
5745
5900
|
key: tickets.key,
|
|
5746
5901
|
version: tickets.version
|
|
5747
5902
|
}).from(tickets).where(eq(tickets.projectId, current.id)).orderBy(asc(tickets.number));
|
|
5748
|
-
await recordAudience(ctx, requireRow((await publishMany(ctx, [
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5903
|
+
await recordAudience(ctx, requireRow((await publishMany(ctx, [
|
|
5904
|
+
...removed.map((ticket) => ({
|
|
5905
|
+
type: "ticket.deleted",
|
|
5906
|
+
ticketId: ticket.id,
|
|
5907
|
+
data: {
|
|
5908
|
+
key: ticket.key,
|
|
5909
|
+
version: ticket.version
|
|
5910
|
+
}
|
|
5911
|
+
})),
|
|
5912
|
+
...await severedRelationEvents(ctx),
|
|
5913
|
+
{
|
|
5914
|
+
type: "project.deleted",
|
|
5915
|
+
ticketId: null,
|
|
5916
|
+
data: {
|
|
5917
|
+
key: current.key,
|
|
5918
|
+
name: current.name,
|
|
5919
|
+
tickets: removed.length
|
|
5920
|
+
}
|
|
5762
5921
|
}
|
|
5763
|
-
|
|
5922
|
+
], false)).at(-1)), readers.map((person) => person.userId));
|
|
5764
5923
|
await ctx.db.delete(tickets).where(eq(tickets.projectId, current.id));
|
|
5765
5924
|
await ctx.db.delete(stagedUploads).where(eq(stagedUploads.projectId, current.id));
|
|
5766
5925
|
await ctx.db.delete(projectLabels).where(eq(projectLabels.projectId, current.id));
|
|
@@ -5997,7 +6156,8 @@ const timeEntryHandlers = {
|
|
|
5997
6156
|
//#region src/public-operations.ts
|
|
5998
6157
|
const listTickets = async (ctx, input) => parseOutput("list_tickets", json(await ticketHandlers.list_tickets(ctx, operations.list_tickets.input.parse({
|
|
5999
6158
|
...input,
|
|
6000
|
-
projectId: project(ctx).id
|
|
6159
|
+
projectId: project(ctx).id,
|
|
6160
|
+
view: "full"
|
|
6001
6161
|
}))));
|
|
6002
6162
|
async function published(ctx, ids) {
|
|
6003
6163
|
if (!ids.length) return /* @__PURE__ */ new Set();
|
|
@@ -6739,6 +6899,36 @@ function requireRecentSignIn(actor, message) {
|
|
|
6739
6899
|
function fail(status, code, message, details) {
|
|
6740
6900
|
throw new ApiError(status, code, message, details);
|
|
6741
6901
|
}
|
|
6902
|
+
function errorEnvelope(error) {
|
|
6903
|
+
if (error instanceof ApiError) return {
|
|
6904
|
+
status: error.status,
|
|
6905
|
+
body: { error: {
|
|
6906
|
+
status: error.status,
|
|
6907
|
+
code: error.code,
|
|
6908
|
+
message: error.message,
|
|
6909
|
+
details: error.details
|
|
6910
|
+
} }
|
|
6911
|
+
};
|
|
6912
|
+
if (error instanceof z.ZodError) return {
|
|
6913
|
+
status: 400,
|
|
6914
|
+
body: { error: {
|
|
6915
|
+
code: "validation_error",
|
|
6916
|
+
message: "Invalid request",
|
|
6917
|
+
details: error.issues.map((issue) => ({
|
|
6918
|
+
path: issue.path.join("."),
|
|
6919
|
+
message: issue.message
|
|
6920
|
+
}))
|
|
6921
|
+
} }
|
|
6922
|
+
};
|
|
6923
|
+
console.error("Kanvil request failed", { name: error instanceof Error ? error.name : typeof error });
|
|
6924
|
+
return {
|
|
6925
|
+
status: 500,
|
|
6926
|
+
body: { error: {
|
|
6927
|
+
code: "internal_error",
|
|
6928
|
+
message: "The request could not be completed"
|
|
6929
|
+
} }
|
|
6930
|
+
};
|
|
6931
|
+
}
|
|
6742
6932
|
function requireRow(row) {
|
|
6743
6933
|
if (!row) fail(404, "not_found", "The requested record was not found");
|
|
6744
6934
|
return row;
|
|
@@ -6770,7 +6960,13 @@ function json(value) {
|
|
|
6770
6960
|
return JSON.parse(JSON.stringify(value, (_, item) => typeof item === "bigint" ? item.toString() : item));
|
|
6771
6961
|
}
|
|
6772
6962
|
const complete = (status) => closedStates.some((closed) => closed === status);
|
|
6773
|
-
const
|
|
6963
|
+
const intakeHoldReason = "Untrusted intake: human triage required";
|
|
6964
|
+
const intakeQuarantined = (ticket) => ticket.holdReason === intakeHoldReason;
|
|
6965
|
+
const intakeOnly = (actor) => actor.scopes.length > 0 && actor.scopes.every((scope) => scope === "intake");
|
|
6966
|
+
const hasScope = (actor, scope, operation) => intakeOnly(actor) ? operation === "create_ticket" : actor.scopes.includes("admin") || actor.scopes.includes(scope) || scope === "read" && actor.scopes.some((value) => value !== "intake");
|
|
6967
|
+
function requireScope(actor, scope, operation) {
|
|
6968
|
+
if (!hasScope(actor, scope, operation)) fail(403, "insufficient_scope", `This credential requires ${scope} permission`);
|
|
6969
|
+
}
|
|
6774
6970
|
const capabilities = (actor, granted) => granted.filter((permission) => hasScope(actor, permissionScope(permission)));
|
|
6775
6971
|
const can = (ctx, permission) => capabilities(ctx.actor, ctx.permissions).includes(permission);
|
|
6776
6972
|
function requirePermission(granted, permission, where) {
|
|
@@ -6821,8 +7017,8 @@ function ticketMatch(identifier) {
|
|
|
6821
7017
|
const key = identifier.toUpperCase(), [, prefix, number] = issuedKey.exec(key) ?? [];
|
|
6822
7018
|
return or(sql`${tickets.id}::text = ${identifier}`, eq(tickets.key, key), sql`${tickets.id} IN (SELECT ta.ticket_id FROM ${ticketAliases} ta WHERE ta.key = ${key})`, prefix && number ? and(eq(tickets.number, Number(number)), sql`${tickets.projectId} IN (SELECT pp.project_id FROM ${projectPrefixes} pp WHERE pp.prefix = ${prefix})`) : void 0);
|
|
6823
7019
|
}
|
|
6824
|
-
async function context(db, actor, scope, projectId, workspaceId, permission = scope === "read" ? null : scope === "admin" ? "project.manage" : "ticket.write") {
|
|
6825
|
-
|
|
7020
|
+
async function context(db, actor, scope, projectId, workspaceId, permission = scope === "read" ? null : scope === "admin" ? "project.manage" : "ticket.write", operation) {
|
|
7021
|
+
requireScope(actor, scope, operation);
|
|
6826
7022
|
const ctx = baseContext(db, actor);
|
|
6827
7023
|
if (workspaceId) {
|
|
6828
7024
|
const access = await requireWorkspace(ctx, workspaceId, permission);
|
|
@@ -6998,11 +7194,26 @@ async function publish(ctx, type, ticketId, data) {
|
|
|
6998
7194
|
data
|
|
6999
7195
|
}]));
|
|
7000
7196
|
}
|
|
7001
|
-
const relationEvents = (type, relation) => [relation.sourceId, relation.targetId].map((ticketId) => ({
|
|
7197
|
+
const relationEvents = (type, relation, sides) => [relation.sourceId, relation.targetId].map((ticketId) => ({
|
|
7002
7198
|
type,
|
|
7003
7199
|
ticketId,
|
|
7004
|
-
data: { relation }
|
|
7200
|
+
data: { relation },
|
|
7201
|
+
projectId: requireRow(sides.find((side) => side.id === ticketId)).projectId
|
|
7005
7202
|
}));
|
|
7203
|
+
const deletedSide = alias(tickets, "deleted_side");
|
|
7204
|
+
const survivingSide = alias(tickets, "surviving_side");
|
|
7205
|
+
async function severedRelationEvents(ctx, ticketIds) {
|
|
7206
|
+
return (await ctx.db.select({
|
|
7207
|
+
relation: relations$1,
|
|
7208
|
+
ticketId: survivingSide.id,
|
|
7209
|
+
projectId: survivingSide.projectId
|
|
7210
|
+
}).from(relations$1).innerJoin(deletedSide, or(eq(deletedSide.id, relations$1.sourceId), eq(deletedSide.id, relations$1.targetId))).innerJoin(survivingSide, or(eq(survivingSide.id, relations$1.sourceId), eq(survivingSide.id, relations$1.targetId))).where(and(ticketIds ? inArray(deletedSide.id, [...ticketIds]) : eq(deletedSide.projectId, project(ctx).id), ne(survivingSide.projectId, project(ctx).id)))).map(({ relation, ticketId, projectId }) => ({
|
|
7211
|
+
type: "relation.removed",
|
|
7212
|
+
ticketId,
|
|
7213
|
+
projectId,
|
|
7214
|
+
data: { relation }
|
|
7215
|
+
}));
|
|
7216
|
+
}
|
|
7006
7217
|
function ownsLiveClaim(ctx, claim) {
|
|
7007
7218
|
return claim.ownerId === ctx.actor.ownerId && claim.expiresAt > /* @__PURE__ */ new Date() && hasScope(ctx.actor, "claim") && ctx.permissions.includes("ticket.write");
|
|
7008
7219
|
}
|
|
@@ -7012,6 +7223,11 @@ async function assertWorkerClaim(ctx, ticket, claimId) {
|
|
|
7012
7223
|
if (claim && (claim.id !== claimId || !ownsLiveClaim(ctx, claim))) fail(409, "claim_conflict", "An active matching claim is required for this worker write");
|
|
7013
7224
|
if (!claim && (claimId || ticket.claimRequired)) fail(409, "claim_conflict", "This ticket requires current ownership for worker writes");
|
|
7014
7225
|
}
|
|
7226
|
+
async function assertDefinitionMigration(ctx, affected) {
|
|
7227
|
+
if (!can(ctx, "project.manage")) fail(403, "forbidden", "Project administration permission is required");
|
|
7228
|
+
if (!ctx.actor.machine || !affected.length) return;
|
|
7229
|
+
if ((await ctx.db.select({ id: claims.id }).from(claims).where(inArray(claims.ticketId, affected.map((ticket) => ticket.id))).limit(1)).length) fail(409, "claim_conflict", "A claimed ticket blocks this definition change");
|
|
7230
|
+
}
|
|
7015
7231
|
function createDomain(database, services = {}) {
|
|
7016
7232
|
const handlers = {
|
|
7017
7233
|
...workspaceHandlers,
|
|
@@ -7089,6 +7305,7 @@ function createDomain(database, services = {}) {
|
|
|
7089
7305
|
const input = spec.input.parse(value);
|
|
7090
7306
|
const projectId = "projectId" in input ? input.projectId : void 0;
|
|
7091
7307
|
const workspaceId = "workspaceId" in input && !workspaceIsFilter.has(name) ? input.workspaceId : void 0;
|
|
7308
|
+
if (intakeOnly(actor)) requireScope(actor, spec.scope, name);
|
|
7092
7309
|
if (actor.machine && sessionOnly.has(name)) fail(403, "session_required", "Use a signed-in account for this action");
|
|
7093
7310
|
if (actor.projectIds && spec.method !== "GET" && !projectId) fail(403, "insufficient_scope", "Project-bound credentials can only change their configured projects");
|
|
7094
7311
|
const outgoing = [];
|
|
@@ -7102,19 +7319,20 @@ function createDomain(database, services = {}) {
|
|
|
7102
7319
|
return parseOutput(name, json(await handler(ctx, input)));
|
|
7103
7320
|
};
|
|
7104
7321
|
const permission = operationPermission(name);
|
|
7105
|
-
if (spec.method === "GET" || remoteReads.has(name)) return invoke(await context(database.db, actor, spec.scope, projectId, workspaceId, permission));
|
|
7322
|
+
if (spec.method === "GET" || remoteReads.has(name)) return invoke(await context(database.db, actor, spec.scope, projectId, workspaceId, permission, name));
|
|
7106
7323
|
const requestId = "requestId" in input ? input.requestId : void 0;
|
|
7107
7324
|
const reader = sourceReads[name];
|
|
7108
|
-
if (reader && !(requestId && (await database.db.select({ id: receipts.requestId }).from(receipts).where(and(eq(receipts.actorId, actor.id), eq(receipts.requestId, requestId))).limit(1)).length)) read = await reader.read(withServices(await context(database.db, actor, spec.scope, projectId, workspaceId, permission)), input);
|
|
7325
|
+
if (reader && !(requestId && (await database.db.select({ id: receipts.requestId }).from(receipts).where(and(eq(receipts.actorId, actor.id), eq(receipts.requestId, requestId))).limit(1)).length)) read = await reader.read(withServices(await context(database.db, actor, spec.scope, projectId, workspaceId, permission, name)), input);
|
|
7109
7326
|
const committed = await locked(async (tx) => {
|
|
7110
7327
|
const hash = createHash("sha256").update(JSON.stringify({
|
|
7111
7328
|
name,
|
|
7112
|
-
input: reader ? reader.receipt(input) : input
|
|
7329
|
+
input: reader ? reader.receipt(input) : input,
|
|
7330
|
+
...intakeOnly(actor) && { intakeCredential: actor.ownerId }
|
|
7113
7331
|
})).digest("hex");
|
|
7114
7332
|
const receipt = requestId ? (await tx.select().from(receipts).where(and(eq(receipts.actorId, actor.id), eq(receipts.requestId, requestId))).limit(1))[0] : void 0;
|
|
7115
7333
|
if (receipt && (receipt.hash !== hash || receipt.operation !== name)) fail(409, "idempotency_conflict", "This requestId was already used for another write");
|
|
7116
7334
|
if (receipt && (name === "delete_project" || name === "delete_workspace")) return parseOutput(name, receipt.result);
|
|
7117
|
-
const ctx = await context(tx, actor, spec.scope, projectId, workspaceId, permission);
|
|
7335
|
+
const ctx = await context(tx, actor, spec.scope, projectId, workspaceId, permission, name);
|
|
7118
7336
|
if (receipt) {
|
|
7119
7337
|
if ((name === "create_time_entry" || name === "update_time_entry" || name === "delete_time_entry") && "ticketId" in input) await getTicket(ctx, input.ticketId);
|
|
7120
7338
|
const prior = parseOutput(name, receipt.result);
|
|
@@ -7606,6 +7824,13 @@ const googleProfileSchema = z.object({
|
|
|
7606
7824
|
picture: z.string().default("")
|
|
7607
7825
|
});
|
|
7608
7826
|
function createAuth(database, config) {
|
|
7827
|
+
const adapter = drizzleAdapter(database.db, {
|
|
7828
|
+
provider: "pg",
|
|
7829
|
+
schema: auth_schema_exports,
|
|
7830
|
+
transaction: true
|
|
7831
|
+
});
|
|
7832
|
+
const verification = new AsyncLocalStorage();
|
|
7833
|
+
const consumed = () => verification.getStore()?.row ?? null;
|
|
7609
7834
|
const instance = createInstance(database, config);
|
|
7610
7835
|
const credentials = socialCredentials(config);
|
|
7611
7836
|
const mail = config.SMTP_URL && config.SMTP_FROM ? createMailSender(config.SMTP_URL, config.SMTP_FROM) : void 0;
|
|
@@ -7640,15 +7865,37 @@ function createAuth(database, config) {
|
|
|
7640
7865
|
disableIdTokenSignIn: true
|
|
7641
7866
|
};
|
|
7642
7867
|
};
|
|
7643
|
-
|
|
7868
|
+
const auth = betterAuth({
|
|
7644
7869
|
appName: "Kanvil",
|
|
7645
7870
|
baseURL: config.KANVIL_URL,
|
|
7646
7871
|
secret: config.BETTER_AUTH_SECRET,
|
|
7647
|
-
database:
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7872
|
+
database: (options) => {
|
|
7873
|
+
const base = adapter(options);
|
|
7874
|
+
const update = async (input) => {
|
|
7875
|
+
const fields = Object.keys(input.update);
|
|
7876
|
+
if (input.model === "apikey" && fields.length > 0 && fields.every((field) => field === "lastRequest" || field === "updatedAt")) return await base.findOne({
|
|
7877
|
+
model: input.model,
|
|
7878
|
+
where: input.where
|
|
7879
|
+
}) ?? consumed();
|
|
7880
|
+
return base.update(input);
|
|
7881
|
+
};
|
|
7882
|
+
const incrementOne = async (input) => {
|
|
7883
|
+
const row = await base.incrementOne(input);
|
|
7884
|
+
const store = input.model === "apikey" ? verification.getStore() : void 0;
|
|
7885
|
+
if (!store) return row;
|
|
7886
|
+
if (row) store.row = row;
|
|
7887
|
+
else if (store.row && !await base.findOne({
|
|
7888
|
+
model: input.model,
|
|
7889
|
+
where: input.where.filter(({ field }) => field === "id")
|
|
7890
|
+
})) return store.row;
|
|
7891
|
+
return row;
|
|
7892
|
+
};
|
|
7893
|
+
return {
|
|
7894
|
+
...base,
|
|
7895
|
+
update,
|
|
7896
|
+
incrementOne
|
|
7897
|
+
};
|
|
7898
|
+
},
|
|
7652
7899
|
trustedOrigins: config.trustedOrigins,
|
|
7653
7900
|
emailAndPassword: {
|
|
7654
7901
|
enabled: true,
|
|
@@ -7797,6 +8044,14 @@ function createAuth(database, config) {
|
|
|
7797
8044
|
}] : [] })
|
|
7798
8045
|
]
|
|
7799
8046
|
});
|
|
8047
|
+
const verifyApiKey = Object.assign((input) => verification.run({}, () => auth.api.verifyApiKey(input)), auth.api.verifyApiKey);
|
|
8048
|
+
return {
|
|
8049
|
+
...auth,
|
|
8050
|
+
api: {
|
|
8051
|
+
...auth.api,
|
|
8052
|
+
verifyApiKey
|
|
8053
|
+
}
|
|
8054
|
+
};
|
|
7800
8055
|
}
|
|
7801
8056
|
//#endregion
|
|
7802
|
-
export {
|
|
8057
|
+
export { stagedUploads as $, requireSession as A, attachmentContent as B, defaultSenders as C, createAccess as D, createHttpSender as E, decodeImage as F, events as G, blobObjects as H, imageResponse as I, notificationPreferences as J, instanceSettings as K, stagedUploadCutoff as L, createTicketSearch as M, createTicketIndex as N, createWriteLimiter as O, createBlobStorage as P, pushDevices as Q, createObjectStore as R, receiveRepositoryWebhook as S, DeliveryError as T, deliveryCheckpoints as U, attachments as V, eventClock as W, profilePhotos as X, notifications as Y, projects as Z, one as _, deliverySecrets as a, matchesOIDCBinding as at, requireRow as b, ownerBlocker as c, createDomain as d, storageBackends as et, errorEnvelope as f, notify as g, json as h, createInstance as i, workspaces as it, readCalendarFeed as j, requireInstanceAdmin as k, appendEvents as l, intakeOnly as m, emailCodesAvailable as n, tickets as nt, otherActiveAdministrator as o, fail as p, members as q, signInProviders as r, workspaceMembers as rt, ownedWorkspaces as s, createAuth as t, subscriptions as tt, assertVersion as u, reachable as v, deliverBatch as w, processImports as x, recipientColumns as y, createPollingJob as z };
|