@sjcrh/proteinpaint-server 2.191.3 → 2.191.4
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/dataset/termdb.test.js +3 -0
- package/package.json +4 -4
- package/src/app.js +496 -411
- package/routes/aiProjectAdmin.js +0 -179
- package/routes/aiProjectSelectedWSImages.js +0 -237
- package/routes/deleteWSITileSelection.js +0 -142
- package/routes/saveWSIAnnotation.js +0 -151
- package/routes/wsimages.js +0 -176
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { FlagStatus, SelectionPrefixes, checkSelectionType } from "#types";
|
|
2
|
-
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
|
-
function init({ genomes }) {
|
|
4
|
-
return async (req, res) => {
|
|
5
|
-
try {
|
|
6
|
-
const query = req.query;
|
|
7
|
-
if (!query.genome) throw new Error(".genome is required for deleteWSIAnnotation request.");
|
|
8
|
-
if (!query.dslabel) throw new Error(".dslabel is required for deleteWSIAnnotation request.");
|
|
9
|
-
const g = genomes[query.genome];
|
|
10
|
-
if (!g) throw new Error("invalid genome name");
|
|
11
|
-
const ds = g.datasets[query.dslabel];
|
|
12
|
-
if (!ds) throw new Error("invalid dataset name");
|
|
13
|
-
if (typeof ds.queries?.WSImages?.saveWSIAnnotation === "function") {
|
|
14
|
-
const result = await ds.queries.WSImages.saveWSIAnnotation(query, req);
|
|
15
|
-
if (result?.status === "error") {
|
|
16
|
-
return res.status(500).send(result);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
res.status(200).send({ status: "ok" });
|
|
20
|
-
} catch (e) {
|
|
21
|
-
console.warn(e);
|
|
22
|
-
res.status(500).send({
|
|
23
|
-
status: "error",
|
|
24
|
-
error: e?.message || String(e)
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
async function validate_query_saveWSIAnnotation(ds) {
|
|
30
|
-
if (!ds.queries?.WSImages?.db) return;
|
|
31
|
-
const connection = getDbConnection(ds);
|
|
32
|
-
if (!connection) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
validateQuery(ds, connection);
|
|
36
|
-
}
|
|
37
|
-
function validateQuery(ds, connection) {
|
|
38
|
-
ds.queries.WSImages.saveWSIAnnotation = async (annotation, req) => {
|
|
39
|
-
try {
|
|
40
|
-
const { email = "" } = req.query.__protected__.clientAuthResult ?? {};
|
|
41
|
-
const tileSelection = annotation.tileSelection;
|
|
42
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
43
|
-
const projectId = annotation.projectId;
|
|
44
|
-
const wsimageFilename = annotation.wsimage;
|
|
45
|
-
const coords = JSON.stringify(tileSelection.zoomCoordinates ?? []);
|
|
46
|
-
const flag = tileSelection.flag;
|
|
47
|
-
const classId = annotation.classId;
|
|
48
|
-
const isAnnotation = checkSelectionType(tileSelection, SelectionPrefixes.Annotation);
|
|
49
|
-
const isPrediction = checkSelectionType(tileSelection, SelectionPrefixes.Prediction);
|
|
50
|
-
const currentUser = connection.prepare("SELECT current_user FROM project WHERE id = ?").get(projectId);
|
|
51
|
-
if (!await ds.queries?.AIHalAuth?.checkAuthorization(req, "annotate", currentUser?.current_user ?? null)) {
|
|
52
|
-
return {
|
|
53
|
-
status: "error",
|
|
54
|
-
error: "logout"
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (!isAnnotation && !isPrediction) {
|
|
58
|
-
return {
|
|
59
|
-
status: "error",
|
|
60
|
-
error: `Invalid tileSelection id "${tileSelection.id}". Must start with "${SelectionPrefixes.Annotation}" or "${SelectionPrefixes.Prediction}".`
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
if (projectId == null || wsimageFilename == null) {
|
|
64
|
-
return {
|
|
65
|
-
status: "error",
|
|
66
|
-
error: "Missing required fields: projectId and wsimage."
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
const getImageIdSql = `
|
|
70
|
-
SELECT id
|
|
71
|
-
FROM project_images
|
|
72
|
-
WHERE project_id = ?
|
|
73
|
-
AND image_path = ?
|
|
74
|
-
LIMIT 1
|
|
75
|
-
`;
|
|
76
|
-
const imageRow = connection.prepare(getImageIdSql).get(projectId, wsimageFilename);
|
|
77
|
-
if (!imageRow?.id) {
|
|
78
|
-
return {
|
|
79
|
-
status: "error",
|
|
80
|
-
error: `Image not found for project_id=${projectId} and image_path="${wsimageFilename}".`
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
const imageId = imageRow.id;
|
|
84
|
-
connection.prepare(
|
|
85
|
-
`DELETE FROM project_flagged_annotations
|
|
86
|
-
WHERE project_id = ?
|
|
87
|
-
AND image_id = ?
|
|
88
|
-
AND coordinates = ?`
|
|
89
|
-
).run(projectId, imageId, coords);
|
|
90
|
-
connection.prepare(
|
|
91
|
-
`DELETE FROM project_flagged_predictions
|
|
92
|
-
WHERE project_id = ?
|
|
93
|
-
AND image_id = ?
|
|
94
|
-
AND coordinates = ?`
|
|
95
|
-
).run(projectId, imageId, coords);
|
|
96
|
-
connection.prepare(
|
|
97
|
-
`DELETE FROM project_annotations
|
|
98
|
-
WHERE project_id = ?
|
|
99
|
-
AND image_id = ?
|
|
100
|
-
AND coordinates = ?`
|
|
101
|
-
).run(projectId, imageId, coords);
|
|
102
|
-
if (isAnnotation) {
|
|
103
|
-
if (tileSelection.flag === FlagStatus.Normal) {
|
|
104
|
-
connection.prepare(
|
|
105
|
-
`
|
|
106
|
-
INSERT INTO project_annotations (
|
|
107
|
-
project_id, user_id, coordinates, timestamp,class_id, image_id
|
|
108
|
-
) VALUES (?, ?, ?, ?, ?, ?)
|
|
109
|
-
`
|
|
110
|
-
).run(projectId, email, coords, timestamp, classId, imageId);
|
|
111
|
-
} else {
|
|
112
|
-
connection.prepare(
|
|
113
|
-
`
|
|
114
|
-
INSERT INTO project_flagged_annotations (
|
|
115
|
-
project_id, user_id, coordinates, timestamp, flagged,class_id, image_id
|
|
116
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
117
|
-
`
|
|
118
|
-
).run(projectId, email, coords, timestamp, flag, classId, imageId);
|
|
119
|
-
}
|
|
120
|
-
} else if (isPrediction) {
|
|
121
|
-
if (tileSelection.flag !== FlagStatus.Normal) {
|
|
122
|
-
const insertSql = `
|
|
123
|
-
INSERT INTO project_flagged_predictions (project_id, user_email, prediction_class_id, coordinates, flag_type,image_id,timestamp)
|
|
124
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
125
|
-
`;
|
|
126
|
-
const insertStmt = connection.prepare(insertSql);
|
|
127
|
-
insertStmt.run(
|
|
128
|
-
annotation.projectId,
|
|
129
|
-
email,
|
|
130
|
-
annotation.classId,
|
|
131
|
-
JSON.stringify(tileSelection.zoomCoordinates),
|
|
132
|
-
tileSelection.flag,
|
|
133
|
-
imageId,
|
|
134
|
-
timestamp
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return { status: "ok" };
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error("Error saving annotation:", error);
|
|
141
|
-
return {
|
|
142
|
-
status: "error",
|
|
143
|
-
error: error?.message || "Failed to save annotation"
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
export {
|
|
149
|
-
init,
|
|
150
|
-
validate_query_saveWSIAnnotation
|
|
151
|
-
};
|
package/routes/wsimages.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import ky from "ky";
|
|
2
|
-
import qs from "qs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { CookieJar } from "tough-cookie";
|
|
5
|
-
import { promisify } from "util";
|
|
6
|
-
import SessionManager from "../src/wsisessions/SessionManager.ts";
|
|
7
|
-
import { ShardManager } from "#src/sharding/ShardManager.ts";
|
|
8
|
-
import { TileServerShardingAlgorithm } from "#src/sharding/TileServerShardingAlgorithm.ts";
|
|
9
|
-
import serverconfig from "#src/serverconfig.js";
|
|
10
|
-
function init({ genomes }) {
|
|
11
|
-
return async (req, res) => {
|
|
12
|
-
try {
|
|
13
|
-
const wSImagesRequest = req.query;
|
|
14
|
-
const g = genomes[wSImagesRequest.genome];
|
|
15
|
-
if (!g) throw new Error("Invalid genome name");
|
|
16
|
-
const ds = g.datasets[wSImagesRequest.dslabel];
|
|
17
|
-
if (!ds) throw new Error("Invalid dataset name");
|
|
18
|
-
const sampleId = wSImagesRequest.sampleId;
|
|
19
|
-
const wsimage = wSImagesRequest.wsimage;
|
|
20
|
-
const aiProjectId = wSImagesRequest.aiProjectId;
|
|
21
|
-
if (!sampleId && (!wsimage || !aiProjectId)) {
|
|
22
|
-
throw new Error("Invalid parameters: sampleId or both wsimage and aiProjectId must be provided");
|
|
23
|
-
}
|
|
24
|
-
const cookieJar = new CookieJar();
|
|
25
|
-
const setCookie = promisify(cookieJar.setCookie.bind(cookieJar));
|
|
26
|
-
const getCookieString = promisify(cookieJar.getCookieString.bind(cookieJar));
|
|
27
|
-
const wsiImagePath = await getWSImagePath(ds, wSImagesRequest);
|
|
28
|
-
const session = await getSessionId(ds, cookieJar, getCookieString, setCookie, wsiImagePath, aiProjectId);
|
|
29
|
-
const getWsiImageResponse = await getWsiImageDimensions(
|
|
30
|
-
session.imageSessionId,
|
|
31
|
-
getCookieString,
|
|
32
|
-
wsiImagePath
|
|
33
|
-
);
|
|
34
|
-
const payload = {
|
|
35
|
-
status: "ok",
|
|
36
|
-
wsiSessionId: session.imageSessionId,
|
|
37
|
-
overlays: session.overlays,
|
|
38
|
-
slide_dimensions: getWsiImageResponse.slide_dimensions,
|
|
39
|
-
mpp: getWsiImageResponse.mpp
|
|
40
|
-
};
|
|
41
|
-
res.status(200).json(payload);
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.warn(e);
|
|
44
|
-
res.status(500).send({
|
|
45
|
-
status: "error",
|
|
46
|
-
error: e.message || e
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
async function getWSImagePath(ds, wSImagesRequest) {
|
|
52
|
-
const mount = serverconfig.features?.tileserver?.mount;
|
|
53
|
-
if (!mount) throw new Error("No mount available for TileServer");
|
|
54
|
-
if (wSImagesRequest.sampleId) {
|
|
55
|
-
return path.join(
|
|
56
|
-
`${mount}/${ds.queries.WSImages.imageBySampleFolder}/${wSImagesRequest.sampleId}`,
|
|
57
|
-
wSImagesRequest.wsimage
|
|
58
|
-
);
|
|
59
|
-
} else {
|
|
60
|
-
return path.join(`${mount}/${ds.queries.WSImages.aiToolImageFolder}/`, wSImagesRequest.wsimage);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function getSessionId(ds, cookieJar, getCookieString, setCookie, wsimage, projectId) {
|
|
64
|
-
const sessionManager = SessionManager.getInstance();
|
|
65
|
-
const maxSessions = serverconfig.features?.tileserver?.maxSessions;
|
|
66
|
-
const maxIdleTime = serverconfig.features?.tileserver?.maxIdleTime;
|
|
67
|
-
const invalidationThreshold = serverconfig.features?.tileserver?.invalidationThreshold;
|
|
68
|
-
const invalidateResult = await sessionManager.syncAndInvalidateSessions(
|
|
69
|
-
wsimage,
|
|
70
|
-
maxSessions,
|
|
71
|
-
maxIdleTime,
|
|
72
|
-
invalidationThreshold
|
|
73
|
-
);
|
|
74
|
-
if (!invalidateResult) throw new Error("Session invalidation failed");
|
|
75
|
-
const session = await sessionManager.getSession(wsimage);
|
|
76
|
-
if (session) {
|
|
77
|
-
return session;
|
|
78
|
-
}
|
|
79
|
-
const tileServer = await sessionManager.getTileServerShard(wsimage);
|
|
80
|
-
if (!tileServer) throw new Error("No TileServer shard available");
|
|
81
|
-
await ky.get(`${tileServer.url}/tileserver/session_id`, {
|
|
82
|
-
timeout: 5e4,
|
|
83
|
-
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
84
|
-
});
|
|
85
|
-
const cookieString = await getCookieString(`${tileServer.url}/tileserver/session_id`);
|
|
86
|
-
const sessionId = cookieString.match(/session_id=([^;]*)/)?.[1];
|
|
87
|
-
if (!sessionId) throw new Error("session_id not found");
|
|
88
|
-
const overlays = [];
|
|
89
|
-
const data = qs.stringify({ slide_path: wsimage });
|
|
90
|
-
await ky.put(`${tileServer.url}/tileserver/slide`, {
|
|
91
|
-
body: data,
|
|
92
|
-
timeout: 5e4,
|
|
93
|
-
headers: {
|
|
94
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
95
|
-
Cookie: `session_id=${sessionId}`
|
|
96
|
-
},
|
|
97
|
-
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
98
|
-
});
|
|
99
|
-
if (ds.queries.WSImages.getPredictionLayers) {
|
|
100
|
-
const predictionLayers = await ds.queries.WSImages.getPredictionLayers(projectId, wsimage);
|
|
101
|
-
if (predictionLayers) {
|
|
102
|
-
const resolveFilename = (key) => {
|
|
103
|
-
if (!predictionLayers) return void 0;
|
|
104
|
-
if (predictionLayers instanceof Map) return predictionLayers.get(key) ?? void 0;
|
|
105
|
-
return predictionLayers[key] ?? void 0;
|
|
106
|
-
};
|
|
107
|
-
const pushOverlayForKey = async (key, predictionOverlayType) => {
|
|
108
|
-
const overlayPath = resolveFilename(key);
|
|
109
|
-
if (!overlayPath) return;
|
|
110
|
-
const annotationsData = qs.stringify({
|
|
111
|
-
overlay_path: overlayPath
|
|
112
|
-
});
|
|
113
|
-
const layerNumber = await ky.put(`${tileServer.url}/tileserver/overlay`, {
|
|
114
|
-
body: annotationsData,
|
|
115
|
-
timeout: 5e4,
|
|
116
|
-
headers: {
|
|
117
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
118
|
-
Cookie: `session_id=${sessionId}`
|
|
119
|
-
},
|
|
120
|
-
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
121
|
-
}).json();
|
|
122
|
-
const overlay = {
|
|
123
|
-
layerNumber,
|
|
124
|
-
predictionOverlayType
|
|
125
|
-
};
|
|
126
|
-
overlays.push(overlay);
|
|
127
|
-
};
|
|
128
|
-
await pushOverlayForKey("Prediction", "Prediction");
|
|
129
|
-
await pushOverlayForKey("Uncertainty", "Uncertainty");
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const sessionData = await sessionManager.setSession(wsimage, sessionId, tileServer, overlays);
|
|
133
|
-
return sessionData;
|
|
134
|
-
}
|
|
135
|
-
async function getWsiImageDimensions(sessionId, getCookieString, wsimage) {
|
|
136
|
-
const shardManager = ShardManager.getInstance();
|
|
137
|
-
const tileServer = await shardManager.shardingAlgorithmsMap?.get(TileServerShardingAlgorithm.TILE_SERVER_SHARDING_KEY)?.getShard(wsimage);
|
|
138
|
-
if (!tileServer) {
|
|
139
|
-
throw new Error("No tile server");
|
|
140
|
-
}
|
|
141
|
-
return await ky.get(`${tileServer.url}/tileserver/slide`, {
|
|
142
|
-
timeout: 12e4,
|
|
143
|
-
hooks: {
|
|
144
|
-
beforeRequest: [
|
|
145
|
-
async (request) => {
|
|
146
|
-
let cookie = await getCookieString(request.url);
|
|
147
|
-
if (!cookie) {
|
|
148
|
-
cookie = `session_id=${sessionId}`;
|
|
149
|
-
}
|
|
150
|
-
request.headers.set("Cookie", cookie);
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
}
|
|
154
|
-
}).json();
|
|
155
|
-
}
|
|
156
|
-
function getHooks(cookieJar, getCookieString, setCookie) {
|
|
157
|
-
return {
|
|
158
|
-
beforeRequest: [
|
|
159
|
-
async (request) => {
|
|
160
|
-
const cookie = await getCookieString(request.url);
|
|
161
|
-
request.headers.set("Cookie", cookie);
|
|
162
|
-
}
|
|
163
|
-
],
|
|
164
|
-
afterResponse: [
|
|
165
|
-
async (request, options, response) => {
|
|
166
|
-
const setCookieHeader = response.headers.get("set-cookie");
|
|
167
|
-
if (setCookieHeader) {
|
|
168
|
-
await setCookie(setCookieHeader, request.url);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
]
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
export {
|
|
175
|
-
init
|
|
176
|
-
};
|