@maudecode/kanvil 0.4.4 → 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-B-Tz8SF4.mjs → auth-Cs_whzgk.mjs} +300 -119
- package/apps/server/dist/client.mjs +1 -1
- package/apps/server/dist/{config-CRADzIvL.mjs → config-DKuNQY5b.mjs} +15 -7
- package/apps/server/dist/index.mjs +98 -30
- package/apps/server/dist/migrate.mjs +2 -2
- package/apps/server/dist/recover.mjs +4 -4
- package/apps/server/dist/{src-BCtZ3wbK.mjs → src-DptOUNRd.mjs} +53 -18
- package/apps/web/dist/assets/{CompositeItem-DvEBmVYM.js → CompositeItem-CXqewtEL.js} +1 -1
- package/apps/web/dist/assets/{Trans-NwgJNiLl.js → Trans-B1rRlWca.js} +1 -1
- package/apps/web/dist/assets/{admin-settings-BVWOe67Z.js → admin-settings-DE3MfBFE.js} +1 -1
- package/apps/web/dist/assets/{archive-restore-j20QLQCl.js → archive-restore-76sStk8K.js} +1 -1
- package/apps/web/dist/assets/az-AZ-bFxpMMZN.js +1 -0
- package/apps/web/dist/assets/{az-Dj0LhQ5O.js → az-B7OgHoaE.js} +1 -1
- package/apps/web/dist/assets/{corner-left-up-BAGDaT_1.js → corner-left-up-PD-73Nae.js} +1 -1
- package/apps/web/dist/assets/{de-BWVFKRmU.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-B26dRk_O.js → delivery-settings-BvMeOsCS.js} +1 -1
- package/apps/web/dist/assets/{el-IYSYQmsa.js → el-BQHQHQgX.js} +1 -1
- package/apps/web/dist/assets/el-GR-SgRHlWhY.js +1 -0
- package/apps/web/dist/assets/{ellipsis-Cx1SIn8a.js → ellipsis-GF1HwTDb.js} +1 -1
- package/apps/web/dist/assets/es-ES-BF1J5vap.js +1 -0
- package/apps/web/dist/assets/{es-C3DJP0aS.js → es-f5Xr8ldr.js} +1 -1
- package/apps/web/dist/assets/{forms-BwtRqvGj.js → forms-BlEL8C_P.js} +1 -1
- package/apps/web/dist/assets/{fr-BlVUPa15.js → fr-DSR0htnl.js} +1 -1
- package/apps/web/dist/assets/fr-FR-D_sldwf6.js +1 -0
- package/apps/web/dist/assets/{hi-CINzKB6O.js → hi-BaAJEWuW.js} +1 -1
- package/apps/web/dist/assets/hi-IN-CfOw4TWf.js +1 -0
- package/apps/web/dist/assets/{highlight-XHbOOx7A.js → highlight-DXT2xgt8.js} +1 -1
- package/apps/web/dist/assets/id-ID-CLKtHO7Q.js +1 -0
- package/apps/web/dist/assets/{id-CpyQwNyO.js → id-kxogWcip.js} +1 -1
- package/apps/web/dist/assets/{index-MTE_naHG.js → index-CYVrs22O.js} +5 -5
- package/apps/web/dist/assets/{invitations-Ch02KEAB.js → invitations-BPik3J_G.js} +1 -1
- package/apps/web/dist/assets/{it-CxMkGyiF.js → it-C_qk40U6.js} +1 -1
- package/apps/web/dist/assets/it-IT-eVZNW5qX.js +1 -0
- package/apps/web/dist/assets/{item-CRS9Ai9p.js → item-TCHuZNK6.js} +1 -1
- package/apps/web/dist/assets/{ja-D72d7qkv.js → ja-Dn5h64V9.js} +1 -1
- package/apps/web/dist/assets/ja-JP-D96njn-P.js +1 -0
- package/apps/web/dist/assets/{ko-BHuNjRQ3.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-tE57CalJ.js → label-settings-DZTorNDm.js} +1 -1
- package/apps/web/dist/assets/{markdown-blocks-CN-hJdJS.js → markdown-blocks-BQLO-H_7.js} +1 -1
- package/apps/web/dist/assets/{member-directory-CoLB-2Oc.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-D3TPH_Mz.js → mk-jLeGD50T.js} +1 -1
- package/apps/web/dist/assets/{nl-BHJbk-Wr.js → nl-AVtC5ygT.js} +1 -1
- package/apps/web/dist/assets/nl-NL-D1tvo0Gd.js +1 -0
- package/apps/web/dist/assets/{pl-JW_P_Gb9.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-DuV33mlL.js → project-lifecycle-n1ZZbYfu.js} +1 -1
- package/apps/web/dist/assets/{project-settings-9yJbfZRu.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-DWL_sEp3.js → pt-BR-Cm04EWM-.js} +1 -1
- package/apps/web/dist/assets/{public-project-M93Zpx_e.js → public-project-BivDjU_a.js} +1 -1
- package/apps/web/dist/assets/{rotate-ccw-DcHaluuA.js → rotate-ccw-CSaKhZYt.js} +1 -1
- package/apps/web/dist/assets/{ru-DQNby7hW.js → ru-IeycXsyE.js} +1 -1
- package/apps/web/dist/assets/ru-RU-4ew92YE_.js +1 -0
- package/apps/web/dist/assets/{search-DQtwggUH.js → search-Ble6WSEU.js} +3 -3
- package/apps/web/dist/assets/{send-gpGpxsCa.js → send-BLMZ8gQ0.js} +1 -1
- package/apps/web/dist/assets/settings-CH48qQK2.js +1 -0
- package/apps/web/dist/assets/{sliders-horizontal-BxgR4pFt.js → sliders-horizontal-Db6kRvuw.js} +1 -1
- package/apps/web/dist/assets/{switch-UPVGS5kv.js → switch-BeX-0Fwq.js} +1 -1
- package/apps/web/dist/assets/{table-D8iAxhJH.js → table-UJp5S9yy.js} +1 -1
- package/apps/web/dist/assets/ticket-detail-C24opfNW.js +2 -0
- package/apps/web/dist/assets/{ticket-editor-B5eOWIj4.js → ticket-editor--d_bG9C1.js} +2 -2
- package/apps/web/dist/assets/{ticket-list-C965rmmf.js → ticket-list-DRbZWYHf.js} +1 -1
- package/apps/web/dist/assets/{ticket-list-controls-ua3m0uRB.js → ticket-list-controls-fYBottFC.js} +1 -1
- package/apps/web/dist/assets/{tr-BNZEkwqd.js → tr-DtT_dKUK.js} +1 -1
- package/apps/web/dist/assets/tr-TR-CM381XCw.js +1 -0
- package/apps/web/dist/assets/{uk-Dmlqvk63.js → uk-CsbeHSS6.js} +1 -1
- package/apps/web/dist/assets/uk-UA-AeOiWCVo.js +1 -0
- package/apps/web/dist/assets/{upload-Cu0J8ZFJ.js → upload-DUEg04Nj.js} +1 -1
- package/apps/web/dist/assets/{useQueries-BEzeS0hU.js → useQueries-5lW2C5Sb.js} +1 -1
- package/apps/web/dist/assets/vi-VN-td08xXv9.js +1 -0
- package/apps/web/dist/assets/{vi-KALEc3bk.js → vi-rkf1BOLx.js} +1 -1
- package/apps/web/dist/assets/{visual-editor-DyhI5fa5.js → visual-editor-DkFLjouD.js} +1 -1
- package/apps/web/dist/assets/{workspace-settings-DGr-Lwnz.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-DfruhRl9.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-C4keUYMa.js → zh-TW-DEnTqjbn.js} +1 -1
- package/apps/web/dist/index.html +7 -7
- package/docs/api.md +16 -6
- 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-D6I-Xfqm.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-Co61IE04.js +0 -1
- package/apps/web/dist/assets/ticket-detail-BKilKtg_.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
|
};
|
|
@@ -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);
|
|
@@ -2417,7 +2450,7 @@ async function createTicketRow(ctx, input) {
|
|
|
2417
2450
|
...fields,
|
|
2418
2451
|
...state,
|
|
2419
2452
|
...dates,
|
|
2420
|
-
customFields: applyCustomFieldValues(await projectCustomFields(ctx), null, customFields),
|
|
2453
|
+
customFields: intakeOnly(ctx.actor) ? {} : applyCustomFieldValues(await projectCustomFields(ctx), null, customFields),
|
|
2421
2454
|
...await allocateTicket(ctx),
|
|
2422
2455
|
id,
|
|
2423
2456
|
createdBy: ctx.actor.id
|
|
@@ -2464,6 +2497,20 @@ const hitRow = z.object({
|
|
|
2464
2497
|
title: z.string().nullable(),
|
|
2465
2498
|
status: z.enum(states).nullable()
|
|
2466
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
|
+
}
|
|
2467
2514
|
function excerpt(body, query, radius = 80) {
|
|
2468
2515
|
const text = body?.replace(/\s+/gu, " ").trim() ?? "", at = text.toLowerCase().indexOf(query.toLowerCase());
|
|
2469
2516
|
if (at < 0) return "";
|
|
@@ -2481,25 +2528,29 @@ async function deleteTicketRows(ctx, selection) {
|
|
|
2481
2528
|
const ids = [...selected.keys()];
|
|
2482
2529
|
const [claimed] = await ctx.db.select({ ticketId: claims.ticketId }).from(claims).where(inArray(claims.ticketId, ids)).limit(1);
|
|
2483
2530
|
if (claimed) fail(409, "claim_conflict", "Release ownership before deleting this ticket", { ticketId: claimed.ticketId });
|
|
2484
|
-
await publishMany(ctx, [
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
+
]);
|
|
2503
2554
|
await ctx.db.delete(tickets).where(inArray(tickets.id, ids));
|
|
2504
2555
|
}
|
|
2505
2556
|
async function relocation(ctx, ticket, destinationProjectId) {
|
|
@@ -2568,6 +2619,16 @@ async function relocation(ctx, ticket, destinationProjectId) {
|
|
|
2568
2619
|
fields
|
|
2569
2620
|
};
|
|
2570
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
|
+
}
|
|
2571
2632
|
const ticketHandlers = {
|
|
2572
2633
|
async list_ticket_labels(ctx, input) {
|
|
2573
2634
|
const labels = labelUsage(ctx, input.archived).as("usage");
|
|
@@ -2578,7 +2639,11 @@ const ticketHandlers = {
|
|
|
2578
2639
|
const related = input.relatedTo ? await getTicket(ctx, input.relatedTo) : void 0;
|
|
2579
2640
|
const search = input.q ? await ctx.search.where(ctx.db, [project(ctx).id], input.q) : void 0;
|
|
2580
2641
|
await assertCustomFieldFilters(ctx, [project(ctx).id], input);
|
|
2581
|
-
|
|
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;
|
|
2582
2647
|
},
|
|
2583
2648
|
async list_my_work(ctx, input) {
|
|
2584
2649
|
const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
|
|
@@ -2607,6 +2672,21 @@ const ticketHandlers = {
|
|
|
2607
2672
|
};
|
|
2608
2673
|
},
|
|
2609
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
|
+
};
|
|
2610
2690
|
const { row, event } = await createTicketRow(ctx, input);
|
|
2611
2691
|
await publishMany(ctx, [event, ...(await claimStagedUploads(ctx, row.id, input.uploads)).map((item) => ({
|
|
2612
2692
|
type: "attachment.created",
|
|
@@ -2652,7 +2732,7 @@ const ticketHandlers = {
|
|
|
2652
2732
|
...event.data,
|
|
2653
2733
|
duplicateOf: source.id
|
|
2654
2734
|
}
|
|
2655
|
-
}, ...relationEvents("relation.added", relation)]);
|
|
2735
|
+
}, ...relationEvents("relation.added", relation, [row, source])]);
|
|
2656
2736
|
return row;
|
|
2657
2737
|
},
|
|
2658
2738
|
update_ticket: updateTicket,
|
|
@@ -2742,10 +2822,7 @@ const ticketHandlers = {
|
|
|
2742
2822
|
return row;
|
|
2743
2823
|
},
|
|
2744
2824
|
async resolve_ticket(ctx, input) {
|
|
2745
|
-
const
|
|
2746
|
-
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) : [];
|
|
2747
|
-
const owner = allowed.find((row) => row.id === ticket?.projectId);
|
|
2748
|
-
if (!ticket || !owner) fail(404, "not_found", "Ticket was not found");
|
|
2825
|
+
const { ticket, project: owner } = await readableTicket(ctx, input.ticketId);
|
|
2749
2826
|
return {
|
|
2750
2827
|
ticket,
|
|
2751
2828
|
project: {
|
|
@@ -2793,17 +2870,20 @@ const ticketHandlers = {
|
|
|
2793
2870
|
async search_tickets(ctx, input) {
|
|
2794
2871
|
const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
|
|
2795
2872
|
if (!allowed.length) return { items: [] };
|
|
2796
|
-
const ids = allowed.map((row) => row.id);
|
|
2797
|
-
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) => {
|
|
2798
2875
|
const owner = allowed.find((row) => row.id === ticket.projectId);
|
|
2799
|
-
|
|
2800
|
-
|
|
2876
|
+
if (!owner) return [];
|
|
2877
|
+
const summary = input.view === "summary", body = `${ticket.description} ${ticket.acceptanceCriteria}`;
|
|
2878
|
+
return [{
|
|
2879
|
+
ticket: summary ? summarize(ticket) : ticket,
|
|
2801
2880
|
project: {
|
|
2802
2881
|
id: owner.id,
|
|
2803
2882
|
key: owner.key,
|
|
2804
2883
|
name: owner.name
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2884
|
+
},
|
|
2885
|
+
...summary && { snippet: snippet(body, input.q) }
|
|
2886
|
+
}];
|
|
2807
2887
|
}) };
|
|
2808
2888
|
},
|
|
2809
2889
|
async search_workspace(ctx, input) {
|
|
@@ -2828,23 +2908,26 @@ const ticketHandlers = {
|
|
|
2828
2908
|
filtered ? void 0 : sql`SELECT 'project' AS type, projects.id::text AS id, projects.id AS project_id,
|
|
2829
2909
|
NULL::uuid AS ticket_id, NULL::text AS actor_id, projects.description AS body,
|
|
2830
2910
|
NULL::text AS event_type, NULL::jsonb AS data, projects.updated_at AS at,
|
|
2831
|
-
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
|
|
2832
2913
|
FROM projects WHERE ${inArray(projects.id, ids)}
|
|
2833
2914
|
AND (projects.name ILIKE ${like} OR projects.key ILIKE ${like} OR projects.description ILIKE ${like})`,
|
|
2834
2915
|
sql`SELECT 'ticket' AS type, tickets.id::text AS id, tickets.project_id,
|
|
2835
2916
|
tickets.id AS ticket_id, NULL::text AS actor_id,
|
|
2836
2917
|
concat_ws(' ', tickets.description, tickets.acceptance_criteria) AS body,
|
|
2837
2918
|
NULL::text AS event_type, NULL::jsonb AS data, tickets.updated_at AS at,
|
|
2838
|
-
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
|
|
2839
2921
|
FROM tickets WHERE ${and(ticketScope, or(ticketMatch(input.q), match.where))}`,
|
|
2840
2922
|
sql`SELECT 'comment' AS type, comments.id::text AS id, comments.project_id,
|
|
2841
2923
|
comments.ticket_id, comments.actor_id, comments.body, NULL::text AS event_type,
|
|
2842
|
-
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
|
|
2843
2926
|
FROM comments JOIN tickets ON tickets.id = comments.ticket_id
|
|
2844
2927
|
WHERE ${and(ticketScope, sql`comments.body ILIKE ${like}`)}`,
|
|
2845
2928
|
sql`SELECT 'activity' AS type, events.id::text AS id, events.project_id,
|
|
2846
2929
|
events.ticket_id, events.actor_id, NULL::text AS body, events.type AS event_type,
|
|
2847
|
-
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
|
|
2848
2931
|
FROM events
|
|
2849
2932
|
LEFT JOIN tickets ON tickets.id = events.ticket_id AND tickets.project_id = events.project_id
|
|
2850
2933
|
LEFT JOIN "user" ON "user".id = events.actor_id
|
|
@@ -2861,14 +2944,15 @@ const ticketHandlers = {
|
|
|
2861
2944
|
FROM (${hits}) AS hits
|
|
2862
2945
|
LEFT JOIN tickets ON tickets.id = hits.ticket_id AND tickets.project_id = hits.project_id
|
|
2863
2946
|
WHERE ${input.type ? sql`hits.type = ${input.type}` : sql`true`}
|
|
2864
|
-
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
|
|
2865
2948
|
LIMIT ${input.limit} OFFSET ${input.offset}`)).rows.map((row) => hitRow.parse(row));
|
|
2866
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;
|
|
2867
2951
|
return {
|
|
2868
2952
|
items: rows.flatMap((row) => {
|
|
2869
2953
|
const owner = projectsById.get(row.project_id);
|
|
2870
2954
|
if (!owner) return [];
|
|
2871
|
-
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) ?? {
|
|
2872
2956
|
id: row.ticket_id,
|
|
2873
2957
|
key: row.key,
|
|
2874
2958
|
title: row.title,
|
|
@@ -2894,6 +2978,7 @@ const ticketHandlers = {
|
|
|
2894
2978
|
createdAt: at
|
|
2895
2979
|
}, true) : ticket?.title ?? "",
|
|
2896
2980
|
excerpt: excerpt(row.body, input.q),
|
|
2981
|
+
...input.view === "summary" && { snippet: snippet(row.body, input.q) },
|
|
2897
2982
|
actorId: row.actor_id,
|
|
2898
2983
|
at
|
|
2899
2984
|
}];
|
|
@@ -3259,6 +3344,13 @@ function mentionedUserIds(markdown) {
|
|
|
3259
3344
|
//#region src/collaboration-operations.ts
|
|
3260
3345
|
const sourceTicket = alias(tickets, "source_ticket");
|
|
3261
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
|
+
}
|
|
3262
3354
|
async function editableComment(ctx, commentId, expectedVersion) {
|
|
3263
3355
|
const row = one(await ctx.db.select().from(comments).where(and(eq(comments.projectId, project(ctx).id), eq(comments.id, commentId))));
|
|
3264
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");
|
|
@@ -3289,18 +3381,45 @@ async function addComment(ctx, input) {
|
|
|
3289
3381
|
const collaborationHandlers = {
|
|
3290
3382
|
async list_relations(ctx, input) {
|
|
3291
3383
|
const ticket = await getTicket(ctx, input.ticketId);
|
|
3292
|
-
|
|
3384
|
+
const rows = await ctx.db.select({
|
|
3293
3385
|
...getTableColumns(relations$1),
|
|
3294
3386
|
sourceKey: sourceTicket.key,
|
|
3295
3387
|
sourceTitle: sourceTicket.title,
|
|
3388
|
+
sourceProjectId: sourceTicket.projectId,
|
|
3389
|
+
sourceProjectKey: sourceProject.key,
|
|
3296
3390
|
targetKey: targetTicket.key,
|
|
3297
|
-
targetTitle: targetTicket.title
|
|
3298
|
-
|
|
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
|
+
});
|
|
3299
3413
|
},
|
|
3300
3414
|
async add_relation(ctx, input) {
|
|
3301
|
-
const current = await getTicket(ctx, input.ticketId), other = await
|
|
3415
|
+
const current = await getTicket(ctx, input.ticketId), { ticket: other } = await readableTicket(ctx, input.targetId);
|
|
3302
3416
|
const [source, target] = input.direction === "incoming" ? [other, current] : [current, other];
|
|
3303
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]);
|
|
3304
3423
|
if (input.type === "blocks") await assertNoCycle(ctx, source.id, target.id);
|
|
3305
3424
|
const [sourceId, targetId] = input.type === "related" ? [source.id, target.id].toSorted() : [source.id, target.id];
|
|
3306
3425
|
const row = one(await ctx.db.insert(relations$1).values({
|
|
@@ -3316,12 +3435,19 @@ const collaborationHandlers = {
|
|
|
3316
3435
|
],
|
|
3317
3436
|
set: { type: input.type }
|
|
3318
3437
|
}).returning());
|
|
3319
|
-
await publishMany(ctx, relationEvents("relation.added", row));
|
|
3438
|
+
await publishMany(ctx, relationEvents("relation.added", row, [current, other]));
|
|
3320
3439
|
return row;
|
|
3321
3440
|
},
|
|
3322
3441
|
async remove_relation(ctx, input) {
|
|
3323
|
-
const
|
|
3324
|
-
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));
|
|
3325
3451
|
return { ok: true };
|
|
3326
3452
|
},
|
|
3327
3453
|
async list_comments(ctx, input) {
|
|
@@ -3974,7 +4100,7 @@ function matches(rule, names, labels) {
|
|
|
3974
4100
|
const has = (id) => carried.has(names.get(id)?.toLowerCase() ?? "\0");
|
|
3975
4101
|
return rule.match === "all" ? rule.labels.every(has) : rule.labels.some(has);
|
|
3976
4102
|
}
|
|
3977
|
-
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));
|
|
3978
4104
|
const workspaceOf = async (db, projectId) => one(await db.select({ workspaceId: projects.workspaceId }).from(projects).where(eq(projects.id, projectId))).workspaceId;
|
|
3979
4105
|
async function setLinks(db, projectId, links, changes) {
|
|
3980
4106
|
if (!links.length) return [];
|
|
@@ -4036,7 +4162,7 @@ async function queueIssueSync(db, rows) {
|
|
|
4036
4162
|
for (const ticket of found) for (const connection of connections) {
|
|
4037
4163
|
const link = links.find((item) => item.connectionId === connection.id && item.ticketId === ticket.id), labelled = names.get(connection.id) ?? /* @__PURE__ */ new Map();
|
|
4038
4164
|
if (!link) {
|
|
4039
|
-
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({
|
|
4040
4166
|
connectionId: connection.id,
|
|
4041
4167
|
ticketId: ticket.id,
|
|
4042
4168
|
origin: "outbound",
|
|
@@ -4057,8 +4183,9 @@ async function queueIssueSync(db, rows) {
|
|
|
4057
4183
|
state: "review",
|
|
4058
4184
|
reason: "labels"
|
|
4059
4185
|
});
|
|
4060
|
-
else if (link.state === "active" && !connection.paused) {
|
|
4186
|
+
else if ((link.state === "active" || link.state === "creating") && qualified && !connection.paused) {
|
|
4061
4187
|
await queueSync(db, [link]);
|
|
4188
|
+
if (link.state !== "active") continue;
|
|
4062
4189
|
const fresh = commentEvents.filter((event) => event.ticketId === ticket.id && !copied.has(`${link.id}:${String(event.data.commentId)}`));
|
|
4063
4190
|
if (fresh.length) await db.insert(deliveries).values(fresh.map((event) => ({
|
|
4064
4191
|
connectionId: connection.id,
|
|
@@ -4311,6 +4438,10 @@ async function sendIssueSync(database, senders, secrets, drivers, job, event) {
|
|
|
4311
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];
|
|
4312
4439
|
if (!loaded) return finish(database.db, job, "link_removed");
|
|
4313
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
|
+
});
|
|
4314
4445
|
const skip = connection.paused ? "paused" : !loaded.writer ? "access_revoked" : loaded.archivedAt ? "project_archived" : ticket.projectId !== connection.projectId ? "moved" : null;
|
|
4315
4446
|
if (skip === "access_revoked" || skip === "project_archived") return locked(database, async (db) => {
|
|
4316
4447
|
await finish(db, job, skip);
|
|
@@ -4525,7 +4656,7 @@ async function plan(ctx, connection, { inbound, outbound }) {
|
|
|
4525
4656
|
if (link.state === "active") (qualified ? matching : pause).push(link);
|
|
4526
4657
|
else if (link.state === "paused" && link.reason === "labels" && qualified) review.push(link);
|
|
4527
4658
|
}
|
|
4528
|
-
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)) : [];
|
|
4529
4660
|
return {
|
|
4530
4661
|
matching,
|
|
4531
4662
|
pause,
|
|
@@ -5769,22 +5900,26 @@ async function removeProject(ctx) {
|
|
|
5769
5900
|
key: tickets.key,
|
|
5770
5901
|
version: tickets.version
|
|
5771
5902
|
}).from(tickets).where(eq(tickets.projectId, current.id)).orderBy(asc(tickets.number));
|
|
5772
|
-
await recordAudience(ctx, requireRow((await publishMany(ctx, [
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
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
|
+
}
|
|
5786
5921
|
}
|
|
5787
|
-
|
|
5922
|
+
], false)).at(-1)), readers.map((person) => person.userId));
|
|
5788
5923
|
await ctx.db.delete(tickets).where(eq(tickets.projectId, current.id));
|
|
5789
5924
|
await ctx.db.delete(stagedUploads).where(eq(stagedUploads.projectId, current.id));
|
|
5790
5925
|
await ctx.db.delete(projectLabels).where(eq(projectLabels.projectId, current.id));
|
|
@@ -6021,7 +6156,8 @@ const timeEntryHandlers = {
|
|
|
6021
6156
|
//#region src/public-operations.ts
|
|
6022
6157
|
const listTickets = async (ctx, input) => parseOutput("list_tickets", json(await ticketHandlers.list_tickets(ctx, operations.list_tickets.input.parse({
|
|
6023
6158
|
...input,
|
|
6024
|
-
projectId: project(ctx).id
|
|
6159
|
+
projectId: project(ctx).id,
|
|
6160
|
+
view: "full"
|
|
6025
6161
|
}))));
|
|
6026
6162
|
async function published(ctx, ids) {
|
|
6027
6163
|
if (!ids.length) return /* @__PURE__ */ new Set();
|
|
@@ -6824,7 +6960,13 @@ function json(value) {
|
|
|
6824
6960
|
return JSON.parse(JSON.stringify(value, (_, item) => typeof item === "bigint" ? item.toString() : item));
|
|
6825
6961
|
}
|
|
6826
6962
|
const complete = (status) => closedStates.some((closed) => closed === status);
|
|
6827
|
-
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
|
+
}
|
|
6828
6970
|
const capabilities = (actor, granted) => granted.filter((permission) => hasScope(actor, permissionScope(permission)));
|
|
6829
6971
|
const can = (ctx, permission) => capabilities(ctx.actor, ctx.permissions).includes(permission);
|
|
6830
6972
|
function requirePermission(granted, permission, where) {
|
|
@@ -6875,8 +7017,8 @@ function ticketMatch(identifier) {
|
|
|
6875
7017
|
const key = identifier.toUpperCase(), [, prefix, number] = issuedKey.exec(key) ?? [];
|
|
6876
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);
|
|
6877
7019
|
}
|
|
6878
|
-
async function context(db, actor, scope, projectId, workspaceId, permission = scope === "read" ? null : scope === "admin" ? "project.manage" : "ticket.write") {
|
|
6879
|
-
|
|
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);
|
|
6880
7022
|
const ctx = baseContext(db, actor);
|
|
6881
7023
|
if (workspaceId) {
|
|
6882
7024
|
const access = await requireWorkspace(ctx, workspaceId, permission);
|
|
@@ -7052,11 +7194,26 @@ async function publish(ctx, type, ticketId, data) {
|
|
|
7052
7194
|
data
|
|
7053
7195
|
}]));
|
|
7054
7196
|
}
|
|
7055
|
-
const relationEvents = (type, relation) => [relation.sourceId, relation.targetId].map((ticketId) => ({
|
|
7197
|
+
const relationEvents = (type, relation, sides) => [relation.sourceId, relation.targetId].map((ticketId) => ({
|
|
7056
7198
|
type,
|
|
7057
7199
|
ticketId,
|
|
7058
|
-
data: { relation }
|
|
7200
|
+
data: { relation },
|
|
7201
|
+
projectId: requireRow(sides.find((side) => side.id === ticketId)).projectId
|
|
7059
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
|
+
}
|
|
7060
7217
|
function ownsLiveClaim(ctx, claim) {
|
|
7061
7218
|
return claim.ownerId === ctx.actor.ownerId && claim.expiresAt > /* @__PURE__ */ new Date() && hasScope(ctx.actor, "claim") && ctx.permissions.includes("ticket.write");
|
|
7062
7219
|
}
|
|
@@ -7148,6 +7305,7 @@ function createDomain(database, services = {}) {
|
|
|
7148
7305
|
const input = spec.input.parse(value);
|
|
7149
7306
|
const projectId = "projectId" in input ? input.projectId : void 0;
|
|
7150
7307
|
const workspaceId = "workspaceId" in input && !workspaceIsFilter.has(name) ? input.workspaceId : void 0;
|
|
7308
|
+
if (intakeOnly(actor)) requireScope(actor, spec.scope, name);
|
|
7151
7309
|
if (actor.machine && sessionOnly.has(name)) fail(403, "session_required", "Use a signed-in account for this action");
|
|
7152
7310
|
if (actor.projectIds && spec.method !== "GET" && !projectId) fail(403, "insufficient_scope", "Project-bound credentials can only change their configured projects");
|
|
7153
7311
|
const outgoing = [];
|
|
@@ -7161,19 +7319,20 @@ function createDomain(database, services = {}) {
|
|
|
7161
7319
|
return parseOutput(name, json(await handler(ctx, input)));
|
|
7162
7320
|
};
|
|
7163
7321
|
const permission = operationPermission(name);
|
|
7164
|
-
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));
|
|
7165
7323
|
const requestId = "requestId" in input ? input.requestId : void 0;
|
|
7166
7324
|
const reader = sourceReads[name];
|
|
7167
|
-
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);
|
|
7168
7326
|
const committed = await locked(async (tx) => {
|
|
7169
7327
|
const hash = createHash("sha256").update(JSON.stringify({
|
|
7170
7328
|
name,
|
|
7171
|
-
input: reader ? reader.receipt(input) : input
|
|
7329
|
+
input: reader ? reader.receipt(input) : input,
|
|
7330
|
+
...intakeOnly(actor) && { intakeCredential: actor.ownerId }
|
|
7172
7331
|
})).digest("hex");
|
|
7173
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;
|
|
7174
7333
|
if (receipt && (receipt.hash !== hash || receipt.operation !== name)) fail(409, "idempotency_conflict", "This requestId was already used for another write");
|
|
7175
7334
|
if (receipt && (name === "delete_project" || name === "delete_workspace")) return parseOutput(name, receipt.result);
|
|
7176
|
-
const ctx = await context(tx, actor, spec.scope, projectId, workspaceId, permission);
|
|
7335
|
+
const ctx = await context(tx, actor, spec.scope, projectId, workspaceId, permission, name);
|
|
7177
7336
|
if (receipt) {
|
|
7178
7337
|
if ((name === "create_time_entry" || name === "update_time_entry" || name === "delete_time_entry") && "ticketId" in input) await getTicket(ctx, input.ticketId);
|
|
7179
7338
|
const prior = parseOutput(name, receipt.result);
|
|
@@ -7670,6 +7829,8 @@ function createAuth(database, config) {
|
|
|
7670
7829
|
schema: auth_schema_exports,
|
|
7671
7830
|
transaction: true
|
|
7672
7831
|
});
|
|
7832
|
+
const verification = new AsyncLocalStorage();
|
|
7833
|
+
const consumed = () => verification.getStore()?.row ?? null;
|
|
7673
7834
|
const instance = createInstance(database, config);
|
|
7674
7835
|
const credentials = socialCredentials(config);
|
|
7675
7836
|
const mail = config.SMTP_URL && config.SMTP_FROM ? createMailSender(config.SMTP_URL, config.SMTP_FROM) : void 0;
|
|
@@ -7704,23 +7865,35 @@ function createAuth(database, config) {
|
|
|
7704
7865
|
disableIdTokenSignIn: true
|
|
7705
7866
|
};
|
|
7706
7867
|
};
|
|
7707
|
-
|
|
7868
|
+
const auth = betterAuth({
|
|
7708
7869
|
appName: "Kanvil",
|
|
7709
7870
|
baseURL: config.KANVIL_URL,
|
|
7710
7871
|
secret: config.BETTER_AUTH_SECRET,
|
|
7711
7872
|
database: (options) => {
|
|
7712
7873
|
const base = adapter(options);
|
|
7713
|
-
const update = (input) => {
|
|
7874
|
+
const update = async (input) => {
|
|
7714
7875
|
const fields = Object.keys(input.update);
|
|
7715
|
-
if (input.model === "apikey" && fields.length > 0 && fields.every((field) => field === "lastRequest" || field === "updatedAt")) return base.findOne({
|
|
7876
|
+
if (input.model === "apikey" && fields.length > 0 && fields.every((field) => field === "lastRequest" || field === "updatedAt")) return await base.findOne({
|
|
7716
7877
|
model: input.model,
|
|
7717
7878
|
where: input.where
|
|
7718
|
-
});
|
|
7879
|
+
}) ?? consumed();
|
|
7719
7880
|
return base.update(input);
|
|
7720
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
|
+
};
|
|
7721
7893
|
return {
|
|
7722
7894
|
...base,
|
|
7723
|
-
update
|
|
7895
|
+
update,
|
|
7896
|
+
incrementOne
|
|
7724
7897
|
};
|
|
7725
7898
|
},
|
|
7726
7899
|
trustedOrigins: config.trustedOrigins,
|
|
@@ -7871,6 +8044,14 @@ function createAuth(database, config) {
|
|
|
7871
8044
|
}] : [] })
|
|
7872
8045
|
]
|
|
7873
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
|
+
};
|
|
7874
8055
|
}
|
|
7875
8056
|
//#endregion
|
|
7876
|
-
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 };
|