@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.
@@ -1,179 +0,0 @@
1
- import { getDbConnection } from "#src/aiHistoDBConnection.ts";
2
- import { runMultiStmtSQL, runSQL } from "#src/runSQLHelpers.ts";
3
- function init({ genomes }) {
4
- return async (req, res) => {
5
- try {
6
- const query = req.query;
7
- if (!query.genome || !query.dslabel) {
8
- throw new Error("Genome and dataset label are required for aiProjectAdmin request.");
9
- }
10
- const g = genomes[query.genome];
11
- const ds = g.datasets[query.dslabel];
12
- if (!ds.queries?.WSImages?.db) throw new Error("WSImages database not found.");
13
- const connection = getDbConnection(ds);
14
- const aiHalAuth = ds.queries?.AIHalAuth;
15
- if (!aiHalAuth) throw new Error("AIHalAuth queries not found in dataset.");
16
- const userEmail = req.query.__protected__.clientAuthResult?.email || "";
17
- if (query.for === "list") {
18
- let projects = getProjects(connection);
19
- if (!aiHalAuth.checkAuthorization(req, "listAllProjects")) {
20
- projects = projects.filter((p) => aiHalAuth.getUsers(connection, p.id).includes(userEmail));
21
- }
22
- res.send(projects);
23
- } else if (query.for === "admin") {
24
- if (req.method === "PUT" || query.project.type === "new" && aiHalAuth.checkAuthorization(req, "addProject"))
25
- addProject(connection, query.project);
26
- else if (req.method === "POST" && aiHalAuth.checkAuthorization(req, "editProject")) {
27
- editProject(connection, query.project);
28
- aiHalAuth.setUser(connection, query.project.id, userEmail || "admin", 12);
29
- } else if (req.method === "DELETE" && aiHalAuth.checkAuthorization(req, "deleteProject"))
30
- deleteProject(connection, query.project.id);
31
- else throw new Error('Invalid request method for="admin" in aiProjectAdmin route.');
32
- let projectId = query.project.id;
33
- if (!projectId) {
34
- const row = connection.prepare(`SELECT id FROM project WHERE name = ?`).get(query.project.name);
35
- if (!row) {
36
- throw new Error(`Project not found: ${query.project.name}`);
37
- }
38
- projectId = row.id;
39
- }
40
- res.status(200).send({
41
- status: "ok",
42
- projectId,
43
- message: `Project ${query.project.name} processed successfully`
44
- });
45
- } else if (query.for === "filterImages") {
46
- const parsedLogin = aiHalAuth.parseLogin(req, connection);
47
- if (parsedLogin.status === "error") throw new Error("Authentication failed: " + parsedLogin.error);
48
- const q = ds.cohort.termdb.q;
49
- const data = await q.getFilteredImages(query.project.filter);
50
- data.selectedImages = await ds.queries?.WSImages?.selectWSIImages();
51
- res.status(200).send({
52
- status: "ok",
53
- data
54
- });
55
- } else if (query.for === "images") {
56
- const parsedLogin = aiHalAuth.parseLogin(req, connection);
57
- if (parsedLogin.status === "error") throw new Error("Authentication failed: " + parsedLogin.error);
58
- const images = getImages(connection, query.project);
59
- res.send({ images, status: "ok" });
60
- } else if (query.for === "logout") {
61
- aiHalAuth.setUser(connection, query.project.id, null);
62
- res.status(200).send({
63
- status: "ok",
64
- message: "User logged out successfully"
65
- });
66
- } else if (query.for === "auth") {
67
- const authorizations = {};
68
- const actions = query.auth || [];
69
- for (const action of actions) {
70
- authorizations[action] = aiHalAuth.checkAuthorization(req, action);
71
- }
72
- res.status(200).send(authorizations);
73
- } else {
74
- res.send({
75
- status: "error",
76
- message: "Invalid request"
77
- });
78
- }
79
- } catch (e) {
80
- console.warn(e);
81
- res.status(500).send({
82
- status: "error",
83
- error: e.message || e
84
- });
85
- }
86
- };
87
- }
88
- function getProjects(connection) {
89
- const sql = "SELECT name, id, current_user FROM project";
90
- return runSQL(connection, sql);
91
- }
92
- function getImages(connection, project) {
93
- if (!project.id) {
94
- const res = connection.prepare(`SELECT id FROM project WHERE name = ?`).get(project.name);
95
- if (!res) throw new Error(`Project not found for name: ${project.name}`);
96
- project.id = res.id;
97
- }
98
- const imageRows = connection.prepare(`SELECT image_path FROM project_images WHERE project_id = ? ORDER BY id ASC`).all(project.id);
99
- return imageRows.map((r) => r.image_path);
100
- }
101
- function editProject(connection, project) {
102
- const stmts = [];
103
- if (!project.id) {
104
- const res = connection.prepare(`SELECT id FROM project WHERE name = ?`).get(project.name);
105
- project.id = res.id;
106
- }
107
- if (project.images) {
108
- stmts.push({
109
- sql: `DELETE FROM project_images WHERE project_id = ? AND image_path NOT IN (${project.images.map(() => "?").join(",") || "''"})`,
110
- params: [[project.id, ...project.images]]
111
- });
112
- const existingImg = connection.prepare(`SELECT 1 FROM project_images WHERE project_id = ? AND image_path = ?`);
113
- const multiParams = [];
114
- for (const img of project.images) {
115
- const exists = existingImg.get(project.id, img);
116
- if (!exists) multiParams.push([project.id, img]);
117
- }
118
- if (multiParams.length > 0) {
119
- const insertImg = `INSERT INTO project_images (project_id, image_path) VALUES (?, ?)`;
120
- stmts.push({ sql: insertImg, params: multiParams });
121
- }
122
- }
123
- if (project.filter) {
124
- stmts.push({
125
- sql: `UPDATE project SET filter = ? WHERE id = ?`,
126
- params: [[JSON.stringify(project.filter), project.id]]
127
- });
128
- }
129
- if (project.classes) {
130
- stmts.push({
131
- sql: `DELETE FROM project_classes WHERE project_id = ? AND name NOT IN (${project.classes.map(() => "?").join(",") || "''"})`,
132
- params: [project.id, ...project.classes.map((c) => c.name)]
133
- });
134
- const existingClasses = connection.prepare(`SELECT 1 FROM project_classes WHERE project_id = ? AND name = ?`);
135
- const multiParams = [];
136
- for (const cls of project.classes) {
137
- const exists = existingClasses.get(project.id, cls.name);
138
- if (!exists) multiParams.push([project.id, cls.name, cls.color, cls.key_shortcut || ""]);
139
- }
140
- if (multiParams.length > 0) {
141
- const insertClass = `INSERT INTO project_classes (project_id, name, color, key_shortcut) VALUES (?, ?, ?, ?)`;
142
- stmts.push({ sql: insertClass, params: multiParams });
143
- }
144
- }
145
- runMultiStmtSQL(connection, stmts, "add");
146
- }
147
- function deleteProject(connection, projectId) {
148
- if (!projectId) throw new Error("Invalid project ID [aiProjectAdmin route deleteProject()]");
149
- const stmts = [
150
- { sql: "DELETE FROM project_flagged_annotations WHERE project_id = ?", params: [[projectId]] },
151
- { sql: "DELETE FROM project_annotations WHERE project_id = ?", params: [[projectId]] },
152
- { sql: "DELETE FROM project_flagged_predictions WHERE project_id = ?", params: [[projectId]] },
153
- { sql: "DELETE FROM project_classes WHERE project_id = ?", params: [[projectId]] },
154
- { sql: "DELETE FROM project_images WHERE project_id = ?", params: [[projectId]] },
155
- { sql: "DELETE FROM project_users WHERE project_id = ?", params: [[projectId]] },
156
- { sql: "DELETE FROM project WHERE id = ?", params: [[projectId]] }
157
- ];
158
- runMultiStmtSQL(connection, stmts, "delete");
159
- }
160
- function addProject(connection, project) {
161
- const projectSql = `INSERT INTO project (name, filter)
162
- VALUES (?, ?)`;
163
- const projectParams = [project.name, JSON.stringify(project.filter)];
164
- const row = runSQL(connection, projectSql, projectParams, "add");
165
- if (project.users) {
166
- const userSql = `INSERT INTO project_users (project_id, email)
167
- VALUES (?, ?)`;
168
- const userParams = project.users.map((email) => [row.lastInsertRowid, email]);
169
- runMultiStmtSQL(connection, [{ sql: userSql, params: userParams }], "add");
170
- }
171
- const classSql = `INSERT INTO project_classes (project_id, label, color, key_shortcut)
172
- VALUES (?, ?, ?, ?)`;
173
- const classParams = project.classes.map((c) => [row.lastInsertRowid, c.label, c.color, c.key_shortcut || ""]);
174
- runMultiStmtSQL(connection, [{ sql: classSql, params: classParams }], "add");
175
- }
176
- export {
177
- getImages,
178
- init
179
- };
@@ -1,237 +0,0 @@
1
- import { createSelectionID, SelectionPrefixes, FlagStatus } 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
- const g = genomes[query.genome];
8
- if (!g) throw "invalid genome name";
9
- const ds = g.datasets[query.dslabel];
10
- if (!ds) throw "invalid dataset name";
11
- const projectId = query.projectId;
12
- const wsimagesFilenamesArg = query.wsimagesFilenames;
13
- let wsimagesFilenames = [];
14
- const wsimages = [];
15
- if (Array.isArray(wsimagesFilenamesArg) && wsimagesFilenamesArg.length === 1 && wsimagesFilenamesArg[0] === "all") {
16
- if (ds.queries?.WSImages?.getAllWSImages) {
17
- try {
18
- wsimagesFilenames = await ds.queries.WSImages.getAllWSImages(projectId);
19
- } catch (e) {
20
- console.error("Failed to get WSI image list via helper, falling back to provided param", e);
21
- }
22
- }
23
- } else {
24
- wsimagesFilenames = wsimagesFilenamesArg;
25
- }
26
- if (ds.queries.WSImages.getWSIAnnotations) {
27
- for (const wsimageFilename of wsimagesFilenames) {
28
- const wsimage = {
29
- filename: wsimageFilename
30
- };
31
- wsimage.annotations = await ds.queries.WSImages.getWSIAnnotations(projectId, wsimageFilename);
32
- wsimage.classes = await ds.queries.WSImages.getAnnotationClasses(projectId);
33
- wsimage.uncertainty = ds.queries?.WSImages?.uncertainty;
34
- wsimage.activePatchColor = ds.queries?.WSImages?.activePatchColor;
35
- if (ds.queries.WSImages.getWSIPredictionPatches) {
36
- const predictions = await ds.queries.WSImages.getWSIPredictionPatches(projectId, wsimageFilename);
37
- const classMap = new Map((wsimage.classes || []).map((c) => [c.id, c.label]));
38
- const annotationKeys = new Set(
39
- (wsimage.annotations || []).map((a) => {
40
- const zc = a.zoomCoordinates || [NaN, NaN];
41
- return `${zc[0]},${zc[1]}`;
42
- }).filter(Boolean)
43
- );
44
- let flaggedPredictions = /* @__PURE__ */ new Map();
45
- if (typeof ds.queries.WSImages.getFlaggedPredictions === "function") {
46
- flaggedPredictions = await ds.queries.WSImages.getFlaggedPredictions(projectId, wsimageFilename);
47
- }
48
- wsimage.predictions = (predictions || []).map((p) => {
49
- const label = classMap.get(p.class) ?? p.class;
50
- const flagged_counterpart = flaggedPredictions.get(JSON.stringify(p.zoomCoordinates));
51
- return {
52
- ...p,
53
- class: label,
54
- timestamp: flagged_counterpart?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
55
- flag: flagged_counterpart?.flag ?? FlagStatus.Normal,
56
- id: createSelectionID(SelectionPrefixes.Prediction, p.zoomCoordinates)
57
- };
58
- }).filter((p) => {
59
- const key = `${p.zoomCoordinates[0]},${p.zoomCoordinates[1]}`;
60
- return !annotationKeys.has(key) && p.flag !== FlagStatus.Deleted;
61
- });
62
- }
63
- wsimages.push(wsimage);
64
- }
65
- }
66
- res.send({ wsimages });
67
- } catch (e) {
68
- console.log(e);
69
- res.status(404).send("Sample images not found");
70
- }
71
- };
72
- }
73
- async function validate_query_getWSIAnnotations(ds) {
74
- if (!ds.queries?.WSImages?.db) return;
75
- const connection = getDbConnection(ds);
76
- if (!connection) return;
77
- validateWSIAnnotationsQuery(ds, connection);
78
- }
79
- async function validate_query_getWSIClassesQuery(ds) {
80
- if (!ds.queries?.WSImages?.db) return;
81
- const connection = getDbConnection(ds);
82
- if (!connection) return;
83
- validateWSIClassesQuery(ds, connection);
84
- }
85
- function validateWSIAnnotationsQuery(ds, connection) {
86
- if (!ds.queries?.WSImages?.db) return;
87
- const GET_ANNOTATIONS_SQL = `
88
- SELECT *
89
- FROM (
90
- SELECT
91
- pa.id,
92
- pa.project_id,
93
- pa.user_id,
94
- pa.coordinates,
95
- pa.timestamp,
96
- pa.flagged,
97
- pc.label AS label
98
- FROM project_flagged_annotations pa
99
- INNER JOIN project_images pi
100
- ON pi.id = pa.image_id
101
- LEFT JOIN project_classes pc
102
- ON pc.id = pa.class_id
103
- WHERE pa.project_id = ?
104
- AND pi.image_path = ?
105
-
106
- UNION ALL
107
-
108
- SELECT
109
- pa.id,
110
- pa.project_id,
111
- pa.user_id,
112
- pa.coordinates,
113
- pa.timestamp,
114
- ${FlagStatus.Normal} AS flagged,
115
- pc.label AS label
116
- FROM project_annotations pa
117
- INNER JOIN project_images pi
118
- ON pi.id = pa.image_id
119
- LEFT JOIN project_classes pc
120
- ON pc.id = pa.class_id
121
- WHERE pa.project_id = ?
122
- AND pi.image_path = ?
123
- )
124
- ORDER BY timestamp DESC, id DESC
125
- `;
126
- const GET_PROJECT_IMAGES_SQL = `
127
- SELECT DISTINCT pi.image_path AS image_path
128
- FROM project_images pi
129
- WHERE pi.project_id = ?
130
- ORDER BY pi.id
131
- `;
132
- const GET_FLAGGED_PREDICTIONS_SQL = `
133
- SELECT
134
- pa.coordinates,
135
- pa.flag_type,
136
- pa.timestamp
137
- FROM project_flagged_predictions pa
138
- INNER JOIN project_images pi
139
- ON pi.id = pa.image_id
140
- WHERE pa.project_id = ?
141
- AND pa.image_id = ?
142
- `;
143
- if (!ds.queries) ds.queries = {};
144
- if (!ds.queries.WSImages) ds.queries.WSImages = {};
145
- ds.queries.WSImages.getWSIAnnotations = async (projectId, filename) => {
146
- try {
147
- const stmt = connection.prepare(GET_ANNOTATIONS_SQL);
148
- const rows = stmt.all(projectId, filename, projectId, filename);
149
- return rows.map((r) => {
150
- let coords = [NaN, NaN];
151
- try {
152
- const parsed = typeof r.coordinates === "string" ? JSON.parse(r.coordinates) : r.coordinates;
153
- if (Array.isArray(parsed) && parsed.length >= 2) {
154
- const x = Number(parsed[0]);
155
- const y = Number(parsed[1]);
156
- if (!Number.isNaN(x) && !Number.isNaN(y)) coords = [x, y];
157
- }
158
- } catch {
159
- }
160
- return {
161
- zoomCoordinates: coords,
162
- class: r.label ?? "",
163
- flag: r.flagged === void 0 ? FlagStatus.Normal : r.flagged,
164
- timestamp: r.timestamp,
165
- id: createSelectionID(SelectionPrefixes.Annotation, coords)
166
- };
167
- });
168
- } catch (error) {
169
- console.error("Error loading annotations:", error);
170
- return [];
171
- }
172
- };
173
- ds.queries.WSImages.getAllWSImages = async (projectId) => {
174
- try {
175
- const stmt = connection.prepare(GET_PROJECT_IMAGES_SQL);
176
- const rows = stmt.all(projectId);
177
- return (rows || []).map((r) => r.image_path);
178
- } catch (error) {
179
- console.error("Error loading project images list:", error);
180
- return [];
181
- }
182
- };
183
- ds.queries.WSImages.getFlaggedPredictions = async (projectId, filename) => {
184
- const predictionMap = /* @__PURE__ */ new Map();
185
- const getImageIdSql = `
186
- SELECT id
187
- FROM project_images
188
- WHERE project_id = ?
189
- AND image_path = ?
190
- LIMIT 1
191
- `;
192
- const getImageStmt = connection.prepare(getImageIdSql);
193
- const imageRow = getImageStmt.get(projectId, filename);
194
- if (!imageRow?.id) {
195
- return predictionMap;
196
- }
197
- const imageId = Math.floor(imageRow.id);
198
- try {
199
- const stmt = connection.prepare(GET_FLAGGED_PREDICTIONS_SQL);
200
- const rows = stmt.all(projectId, imageId) || [];
201
- for (const p of rows) {
202
- if (p.coordinates && typeof p.coordinates === "string" && typeof p.flag_type === "number")
203
- predictionMap.set(p.coordinates, { flag: p.flag_type, timestamp: p.timestamp });
204
- }
205
- return predictionMap;
206
- } catch (error) {
207
- console.error("Error loading flagged predictions:", error);
208
- return predictionMap;
209
- }
210
- };
211
- }
212
- function validateWSIClassesQuery(ds, connection) {
213
- if (!ds.queries) ds.queries = {};
214
- if (!ds.queries.WSImages) ds.queries.WSImages = {};
215
- const GET_CLASSES_SQL = `
216
- SELECT id, project_id, label, color, key_shortcut
217
- FROM project_classes
218
- WHERE project_id = ?
219
- ORDER BY id
220
- `;
221
- ds.queries.WSImages.getAnnotationClasses = async (projectId) => {
222
- try {
223
- const stmt = connection.prepare(GET_CLASSES_SQL);
224
- return stmt.all(projectId);
225
- } catch (error) {
226
- console.error("Error loading project classes:", error);
227
- return [];
228
- }
229
- };
230
- }
231
- export {
232
- init,
233
- validateWSIAnnotationsQuery,
234
- validateWSIClassesQuery,
235
- validate_query_getWSIAnnotations,
236
- validate_query_getWSIClassesQuery
237
- };
@@ -1,142 +0,0 @@
1
- import { checkSelectionType, SelectionPrefixes, FlagStatus } 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 deleteWSITileSelection request.");
8
- if (!query.dslabel) throw new Error(".dslabel is required for deleteWSITileSelection request.");
9
- if (!query.tileSelection) throw new Error(".annotation:{} is required for deleteWSITileSelection request.");
10
- if (!query.projectId) throw new Error(".projectId is required for deleteWSITileSelection request.");
11
- if (!query.wsimage) throw new Error(".wsimage is required for deleteWSITileSelection request.");
12
- const g = genomes[query.genome];
13
- if (!g) throw new Error("invalid genome name");
14
- const ds = g.datasets[query.dslabel];
15
- if (!ds) throw new Error("invalid dataset name");
16
- if (typeof ds.queries?.WSImages?.deleteAnnotation === "function") {
17
- const result = await ds.queries.WSImages.deleteAnnotation(query, req);
18
- if (result?.status === "error") {
19
- return res.status(500).send(result);
20
- }
21
- }
22
- res.status(200).send({ status: `Annotation = ${query.tileSelection.zoomCoordinates} deleted.` });
23
- } catch (e) {
24
- console.warn(e);
25
- res.status(500).send({
26
- status: "error",
27
- error: e?.message || String(e)
28
- });
29
- }
30
- };
31
- }
32
- async function validate_query_deleteWSIAnnotation(ds) {
33
- if (!ds.queries?.WSImages?.db) return;
34
- const connection = getDbConnection(ds);
35
- if (!connection) {
36
- return;
37
- }
38
- validateQuery(ds, connection);
39
- }
40
- function validateQuery(ds, connection) {
41
- ds.queries.WSImages.deleteAnnotation = async (query, req) => {
42
- const zoomCoordinates = JSON.stringify(query.tileSelection.zoomCoordinates);
43
- const { current_user: currentUser } = connection.prepare("SELECT current_user FROM project WHERE id = ?").get(query.projectId);
44
- if (!await ds.queries?.AIHalAuth?.checkAuthorization(req, "annotate", currentUser)) {
45
- return {
46
- status: "error",
47
- error: "logout"
48
- };
49
- }
50
- const email = req.query.__protected__.clientAuthResult?.email || "";
51
- if (checkSelectionType(query.tileSelection, SelectionPrefixes.Prediction) && query.tileSelection.flag !== FlagStatus.Normal) {
52
- try {
53
- const projectId = query.projectId;
54
- if (projectId == null) {
55
- return {
56
- status: "error",
57
- error: "Missing required field: projectId"
58
- };
59
- }
60
- const predictionId = query.classID;
61
- const flagType = query.tileSelection.flag;
62
- if (predictionId == null) {
63
- return {
64
- status: "error",
65
- error: "Missing prediction id in tileSelection (expected predictionId, prediction.id, or id)."
66
- };
67
- }
68
- const getImageIdSql = `
69
- SELECT id FROM project_images
70
- WHERE project_id = ? AND image_path = ? LIMIT 1
71
- `;
72
- const imageRow = connection.prepare(getImageIdSql).get(projectId, query.wsimage);
73
- const imageId = imageRow?.id;
74
- if (!imageId) {
75
- return { status: "error", error: "Image not found" };
76
- }
77
- connection.prepare(
78
- `
79
- DELETE FROM project_flagged_predictions
80
- WHERE project_id = ? AND coordinates = ?
81
- AND image_id = (
82
- SELECT id FROM project_images
83
- WHERE project_id = ?
84
- AND image_path = ?
85
- )
86
- `
87
- ).run(projectId, zoomCoordinates, projectId, query.wsimage);
88
- connection.prepare(
89
- `
90
- INSERT INTO project_flagged_predictions
91
- (project_id, user_email, prediction_class_id, coordinates, flag_type, timestamp, image_id)
92
- VALUES (?, ?, ?, ?, ?, ?, ?)
93
- `
94
- ).run(projectId, email, predictionId, zoomCoordinates, flagType, (/* @__PURE__ */ new Date()).toISOString(), imageId);
95
- return { status: "ok" };
96
- } catch (error) {
97
- console.error("Error inserting flagged prediction:", error);
98
- return {
99
- status: "error",
100
- error: error?.message || "Failed to insert flagged prediction"
101
- };
102
- }
103
- } else
104
- try {
105
- connection.prepare(
106
- `
107
- DELETE FROM project_flagged_annotations
108
- WHERE project_id = ?
109
- AND coordinates = ?
110
- AND image_id = (
111
- SELECT id FROM project_images
112
- WHERE project_id = ?
113
- AND image_path = ?
114
- )
115
- `
116
- ).run(query.projectId, zoomCoordinates, query.projectId, query.wsimage);
117
- connection.prepare(
118
- `
119
- DELETE FROM project_annotations
120
- WHERE project_id = ?
121
- AND coordinates = ?
122
- AND image_id = (
123
- SELECT id FROM project_images
124
- WHERE project_id = ?
125
- AND image_path = ?
126
- )
127
- `
128
- ).run(query.projectId, zoomCoordinates, query.projectId, query.wsimage);
129
- return { status: "ok" };
130
- } catch (error) {
131
- console.error("Error deleting annotation:", error);
132
- return {
133
- status: "error",
134
- error: error?.message || "Failed to delete annotation"
135
- };
136
- }
137
- };
138
- }
139
- export {
140
- init,
141
- validate_query_deleteWSIAnnotation
142
- };