Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,9 @@ MIXPANEL_TOKEN=

# Referee Finder
ML_REFEREE_GET_PRESIGNED_URL=
ML_REFEREE_TRIGGER_CID=
ML_REFEREE_TRIGGER_START=
ML_REFEREE_FINDER_RESULT=
ML_REFEREE_FILE_PREFIX=

# Temp var for metering comms between nextjs api backend/nodes backend
INTERNAL_SERVICE_SECRET=psssssssssst
Expand Down
3 changes: 2 additions & 1 deletion desci-server/kubernetes/deployment_dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ spec:
export AWS_SQS_DATA_MIGRATION_QUEUE_URL="{{ .Data.AWS_SQS_DATA_MIGRATION_QUEUE_URL }}"
export AWS_SQS_ML_TOOL_QUEUE_URL="{{ .Data.AWS_SQS_ML_TOOL_QUEUE_URL }}"
export ML_REFEREE_GET_PRESIGNED_URL="{{ .Data.ML_REFEREE_GET_PRESIGNED_URL }}"
export ML_REFEREE_TRIGGER_CID="{{ .Data.ML_REFEREE_TRIGGER_CID }}"
export ML_REFEREE_TRIGGER_START="{{ .Data.ML_REFEREE_TRIGGER_START }}"
export ML_REFEREE_FINDER_RESULT="{{ .Data.ML_REFEREE_FINDER_RESULT }}"
export ML_REFEREE_FILE_PREFIX="{{ .Data.ML_REFEREE_FILE_PREFIX }}"
export ENABLE_GUEST_MODE="{{ .Data.ENABLE_GUEST_MODE }}"
export MIXPANEL_TOKEN="{{ .Data.MIXPANEL_TOKEN }}"
export SCIWEAVE_GOOGLE_CLIENT_ID="{{ .Data.SCIWEAVE_GOOGLE_CLIENT_ID }}"
Expand Down
3 changes: 2 additions & 1 deletion desci-server/kubernetes/deployment_prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ spec:
export AWS_SQS_DATA_MIGRATION_QUEUE_URL="{{ .Data.AWS_SQS_DATA_MIGRATION_QUEUE_URL }}"
export AWS_SQS_ML_TOOL_QUEUE_URL="{{ .Data.AWS_SQS_ML_TOOL_QUEUE_URL }}"
export ML_REFEREE_GET_PRESIGNED_URL="{{ .Data.ML_REFEREE_GET_PRESIGNED_URL }}"
export ML_REFEREE_TRIGGER_CID="{{ .Data.ML_REFEREE_TRIGGER_CID }}"
export ML_REFEREE_TRIGGER_START="{{ .Data.ML_REFEREE_TRIGGER_START }}"
export ML_REFEREE_FINDER_RESULT="{{ .Data.ML_REFEREE_FINDER_RESULT }}"
export ML_REFEREE_FILE_PREFIX="{{ .Data.ML_REFEREE_FILE_PREFIX }}"
export ENABLE_GUEST_MODE="{{ .Data.ENABLE_GUEST_MODE }}"
export MIXPANEL_TOKEN="{{ .Data.MIXPANEL_TOKEN }}"
export SCIWEAVE_GOOGLE_CLIENT_ID="{{ .Data.SCIWEAVE_GOOGLE_CLIENT_ID }}"
Expand Down
3 changes: 2 additions & 1 deletion desci-server/kubernetes/deployment_staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ spec:
export AWS_SQS_DATA_MIGRATION_QUEUE_URL="{{ .Data.AWS_SQS_DATA_MIGRATION_QUEUE_URL }}"
export AWS_SQS_ML_TOOL_QUEUE_URL="{{ .Data.AWS_SQS_ML_TOOL_QUEUE_URL }}"
export ML_REFEREE_GET_PRESIGNED_URL="{{ .Data.ML_REFEREE_GET_PRESIGNED_URL }}"
export ML_REFEREE_TRIGGER_CID="{{ .Data.ML_REFEREE_TRIGGER_CID }}"
export ML_REFEREE_TRIGGER_START="{{ .Data.ML_REFEREE_TRIGGER_START }}"
export ML_REFEREE_FINDER_RESULT="{{ .Data.ML_REFEREE_FINDER_RESULT }}"
export ML_REFEREE_FILE_PREFIX="{{ .Data.ML_REFEREE_FILE_PREFIX }}"
export ENABLE_GUEST_MODE="{{ .Data.ENABLE_GUEST_MODE }}"
export MIXPANEL_TOKEN="{{ .Data.MIXPANEL_TOKEN }}"
export SCIWEAVE_GOOGLE_CLIENT_ID="{{ .Data.SCIWEAVE_GOOGLE_CLIENT_ID }}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ExternalApiUsage" ADD COLUMN "queryingData" JSONB;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- CreateIndex: Add GIN index for ExternalApiUsage.queryingData field
-- This allows efficient JSON containment queries on the queryingData field

CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_external_api_usage_querying_data_gin"
ON "ExternalApiUsage" USING GIN ("queryingData");

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "idx_external_api_usage_querying_data_gin";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Drop the index CONCURRENTLY (and defensively).

Dropping a large GIN index with DROP INDEX can take an ACCESS EXCLUSIVE lock and block writes. Prefer DROP INDEX CONCURRENTLY IF EXISTS to avoid long blocking in production.

Apply this diff:

-DROP INDEX "idx_external_api_usage_querying_data_gin";
+DROP INDEX CONCURRENTLY IF EXISTS "idx_external_api_usage_querying_data_gin";

Note: CONCURRENTLY cannot run inside a transaction. Ensure this migration is executed without a surrounding transaction.

Verify other migrations with CONCURRENTLY (create/drop) and ensure none are wrapped in transactions:


🏁 Script executed:

#!/bin/bash
rg -nP 'CREATE\s+INDEX\s+CONCURRENTLY|DROP\s+INDEX\s+CONCURRENTLY' desci-server/prisma/migrations -C2

Length of output: 830


Drop the GIN index without blocking writes

To avoid an ACCESS EXCLUSIVE lock when removing a large GIN index, switch to DROP INDEX CONCURRENTLY IF EXISTS.

• File (desci-server/prisma/migrations/20250818194118_gin_index_ext_api_querying_data/migration.sql), line 2

-DROP INDEX "idx_external_api_usage_querying_data_gin";
+DROP INDEX CONCURRENTLY IF EXISTS "idx_external_api_usage_querying_data_gin";

• Note: CONCURRENTLY cannot run inside a transaction—ensure this migration is executed in QUERY_RUN_IN_TRANSACTION = false mode or otherwise unwrapped.

I’ve verified that the only other GIN-index migration uses CREATE INDEX CONCURRENTLY IF NOT EXISTS (20250818192759_add_gin_index_external_api_querying_data), and no other drops are currently concurrent.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DROP INDEX "idx_external_api_usage_querying_data_gin";
DROP INDEX CONCURRENTLY IF EXISTS "idx_external_api_usage_querying_data_gin";
🤖 Prompt for AI Agents
In
desci-server/prisma/migrations/20250818194118_gin_index_ext_api_querying_data/migration.sql
around line 2, the migration currently does a blocking DROP INDEX; replace it
with a non-blocking DROP INDEX CONCURRENTLY IF EXISTS
"idx_external_api_usage_querying_data_gin" and ensure the migration runs outside
a transaction (set QUERY_RUN_IN_TRANSACTION = false or otherwise unwrap the
migration) because CONCURRENTLY cannot run inside a transaction.

13 changes: 7 additions & 6 deletions desci-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1435,12 +1435,13 @@ model JournalFormResponse {
}

model ExternalApiUsage {
id Int @id @default(autoincrement())
userId Int
apiType ExternalApi
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
data Json?
id Int @id @default(autoincrement())
userId Int
apiType ExternalApi
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
data Json? // Can dump additional metadata here
queryingData Json? // GIN indexed, important data thats used for querying goes here, avoid dumping random metadata.

// Relationships
user User @relation(fields: [userId], references: [id])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ExternalApi } from '@prisma/client';
import { Response } from 'express';
import { z } from 'zod';

import { prisma } from '../../../client.js';
import { sendSuccess, sendError } from '../../../core/api.js';
import { AuthenticatedRequest } from '../../../core/types.js';
import { logger as parentLogger } from '../../../logger.js';
Expand Down Expand Up @@ -29,32 +31,33 @@ export const getResults = async (req: AuthenticatedRequest, res: Response) => {
'Fetching referee recommendation results',
);

// Verify user has access to this filename by checking session
const sessionResult = await RefereeRecommenderService.getSession(UploadedFileName, user.id);
if (sessionResult.isErr()) {
logger.warn(
{
userId: user.id,
uploadedFileName: UploadedFileName,
// Check ExternalApiUsage table for completed results
const existingResult = await prisma.externalApiUsage.findFirst({
where: {
userId: user.id,
apiType: ExternalApi.REFEREE_FINDER,
queryingData: {
path: ['fileName'],
equals: UploadedFileName,
},
'Session not found or expired',
);
return sendError(res, 'Results not found or access denied', 404);
}
},
orderBy: {
createdAt: 'desc', // Get the most recent result
},
});

const session = sessionResult.value;
if (session.userId !== user.id) {
if (!existingResult) {
logger.warn(
{
userId: user.id,
sessionUserId: session.userId,
uploadedFileName: UploadedFileName,
},
'User does not have access to this session',
'No results found for this filename and user',
);
return sendError(res, 'Results not found or access denied', 404);
return sendError(res, 'Results not found', 404);
}

// User has access to this filename, now fetch actual results from lambda API
const result = await RefereeRecommenderService.getRefereeResults(UploadedFileName);

if (result.isErr()) {
Expand All @@ -63,8 +66,9 @@ export const getResults = async (req: AuthenticatedRequest, res: Response) => {
error: result.error,
userId: user.id,
uploadedFileName: UploadedFileName,
resultId: existingResult.id,
},
'Failed to fetch referee results',
'Failed to fetch referee results from lambda API',
);
return sendError(res, result.error.message, 500);
}
Expand All @@ -75,9 +79,10 @@ export const getResults = async (req: AuthenticatedRequest, res: Response) => {
{
userId: user.id,
uploadedFileName: UploadedFileName,
status: responseData.status,
resultId: existingResult.id,
createdAt: existingResult.createdAt,
},
'Successfully fetched referee recommendation results',
'Successfully fetched referee recommendation results from database',
);

return sendSuccess(res, responseData);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ExternalApi } from '@prisma/client';
import { Response } from 'express';
import { z } from 'zod';

import { prisma } from '../../../client.js';
import { sendSuccess, sendError } from '../../../core/api.js';
import { AuthenticatedRequest } from '../../../core/types.js';
import { logger as parentLogger } from '../../../logger.js';
Expand All @@ -10,6 +12,7 @@ const logger = parentLogger.child({ module: 'RefereeRecommender::PresignedUrlCon

const generatePresignedUrlSchema = z.object({
body: z.object({
fileHash: z.string().min(1, 'fileHash is required'),
fileName: z.string().min(1, 'fileName is required'),
}),
});
Comment on lines 13 to 18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid duplicating request schemas; centralize and add strict hash validation.

This file introduces its own generatePresignedUrlSchema, which diverges from src/schemas/externalApi.schema.ts. Duplication risks drift with docs and other controllers.

  • Reuse the central schema and update it to include fileHash with a strict hex pattern (64 chars).
  • Import the central schema here instead of redefining.
-import { z } from 'zod';
+import { z } from 'zod';
+import { generatePresignedUrlSchema } from '../../../schemas/externalApi.schema.js';
 
-const generatePresignedUrlSchema = z.object({
-  body: z.object({
-    fileHash: z.string().min(1, 'fileHash is required'),
-    fileName: z.string().min(1, 'fileName is required'),
-  }),
-});

Companion change in src/schemas/externalApi.schema.ts (see that file’s comment) to add fileHash and the regex.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const generatePresignedUrlSchema = z.object({
body: z.object({
fileHash: z.string().min(1, 'fileHash is required'),
fileName: z.string().min(1, 'fileName is required'),
}),
});
import { z } from 'zod';
import { generatePresignedUrlSchema } from '../../../schemas/externalApi.schema.js';
🤖 Prompt for AI Agents
In
desci-server/src/controllers/externalApi/RefereeRecommender/issuePresignedUrl.ts
around lines 13-18, a local generatePresignedUrlSchema is being defined which
duplicates schema logic; remove this local definition and import and use the
centralized schema from src/schemas/externalApi.schema.ts instead. Update
src/schemas/externalApi.schema.ts to add the fileHash field under the
appropriate request body schema with a strict hex regex /^[a-fA-F0-9]{64}$/ and
retain the existing fileName validation, export the updated schema, then replace
the local schema usage in this controller with the imported centralized one and
adjust imports/types accordingly.

Expand All @@ -24,17 +27,51 @@ export const generatePresignedUrl = async (req: AuthenticatedRequest, res: Respo
}

const { body } = generatePresignedUrlSchema.parse(req);
const { fileName } = body;
const { fileHash, fileName } = body;

logger.info(
{
userId: user.id,
fileHash,
fileName,
},
'Generating presigned URL for referee recommender',
'Checking cache and potentially generating presigned URL for referee recommender',
);

// Validate file extension (assuming PDFs for now)
// First, check if we have cached results for this fileHash
const cachedResult = await prisma.externalApiUsage.findFirst({
where: {
userId: user.id,
apiType: ExternalApi.REFEREE_FINDER,
queryingData: {
path: ['fileHash'],
equals: fileHash,
},
},
orderBy: {
createdAt: 'desc', // Get the most recent result
},
});

if (cachedResult) {
logger.info(
{
userId: user.id,
fileHash,
cachedResultId: cachedResult.id,
},
'Found cached results for fileHash',
);

return sendSuccess(res, {
cached: true,
message: 'Results for this file are already available',
resultKey: RefereeRecommenderService.prepareFormattedFileName(fileHash),
});
}
Comment on lines +66 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return an UploadedFileName (or equivalent) alongside resultKey for GET compatibility.

/services/ai/referee-recommender/results expects UploadedFileName. Here you return only resultKey derived from fileHash. Unless resultKey === UploadedFileName, clients won’t be able to call GET without extra knowledge.

-      return sendSuccess(res, {
+      const resultKey = RefereeRecommenderService.prepareFormattedFileName(fileHash);
+      return sendSuccess(res, {
         cached: true,
         message: 'Results for this file are already available',
-        resultKey: RefereeRecommenderService.prepareFormattedFileName(fileHash),
+        resultKey,
+        // For compatibility with GET /results
+        UploadedFileName: resultKey,
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return sendSuccess(res, {
cached: true,
message: 'Results for this file are already available',
resultKey: RefereeRecommenderService.prepareFormattedFileName(fileHash),
});
}
const resultKey = RefereeRecommenderService.prepareFormattedFileName(fileHash);
return sendSuccess(res, {
cached: true,
message: 'Results for this file are already available',
resultKey,
// For compatibility with GET /results
UploadedFileName: resultKey,
});
🤖 Prompt for AI Agents
In
desci-server/src/controllers/externalApi/RefereeRecommender/issuePresignedUrl.ts
around lines 66 to 71, the response returns only resultKey (prepared from
fileHash) but the downstream endpoint /services/ai/referee-recommender/results
expects UploadedFileName; add an uploadedFileName property to the success
payload (either use the original uploaded filename from the request/body or
derive/format it the same way the results service expects) so clients can call
the GET endpoint without extra knowledge, and update the response type/interface
to include UploadedFileName.


// No cached results found, proceed with generating presigned URL
// Validate file extension
if (!fileName.toLowerCase().endsWith('.pdf')) {
return sendError(res, 'Only PDF files are supported', 400);
}
Expand All @@ -49,7 +86,7 @@ export const generatePresignedUrl = async (req: AuthenticatedRequest, res: Respo
return sendError(res, 'Failed to generate presigned URL', 500);
}

const { presignedUrl, fileName: generatedFileName } = result.value;
const { presignedUrl, downloadUrl, fileName: generatedFileName } = result.value;

logger.info(
{
Expand All @@ -60,9 +97,11 @@ export const generatePresignedUrl = async (req: AuthenticatedRequest, res: Respo
);

return sendSuccess(res, {
cached: false,
presignedUrl,
downloadUrl,
fileName: generatedFileName,
expiresIn: 3600, // 1 hour
fileHash,
});
} catch (error) {
if (error instanceof z.ZodError) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ExternalApi } from '@prisma/client';
import { Response } from 'express';
import _ from 'lodash';
import { z } from 'zod';

import { prisma } from '../../../client.js';
import { sendSuccess, sendError } from '../../../core/api.js';
import { AuthenticatedRequest } from '../../../core/types.js';
import { logger as parentLogger } from '../../../logger.js';
Expand All @@ -19,18 +22,69 @@ export const triggerRecommendation = async (req: AuthenticatedRequest, res: Resp
}

const { body } = triggerRefereeRecommendationSchema.parse(req);
const { fileUrl, fileHash } = body;

logger.info(
{
userId: user.id,
cid: body.cid,
external: body.external,
fileUrl,
fileHash,
},
'Triggering referee recommendation',
'Checking cache and potentially triggering referee recommendation',
);

// Check if we have cached results for this fileHash (if provided)
let cachedResult = null;

if (fileHash) {
cachedResult = await prisma.externalApiUsage.findFirst({
where: {
userId: user.id,
apiType: ExternalApi.REFEREE_FINDER,
queryingData: {
path: ['fileHash'],
equals: fileHash,
},
},
orderBy: {
createdAt: 'desc', // Get the most recent result
},
});

if (cachedResult) {
logger.info(
{
userId: user.id,
fileHash,
cachedResultId: cachedResult.id,
},
'Found cached results for fileHash, returning cached data',
);

return sendSuccess(res, {
cached: true,
message: 'Results for this file are already available',
createdAt: cachedResult.createdAt,
resultKey: RefereeRecommenderService.prepareFormattedFileName(fileHash),
});
}
}

const result = await RefereeRecommenderService.triggerRefereeRecommendation(
body as typeof body & { cid: string },
{
// hash_value,
// hash_verified,
file_url: body.fileUrl,
top_n_closely_matching: body.top_n_closely_matching,
number_referees: body.number_referees,
force_run: body.force_run,
classify: body.classify,
coi_filter: body.coi_filter,
meta_data_only: body.meta_data_only,
exclude_fields: body.exclude_fields,
exclude_works: body.exclude_works,
exclude_authors: body.exclude_authors,
},
user.id,
);

Expand All @@ -39,7 +93,6 @@ export const triggerRecommendation = async (req: AuthenticatedRequest, res: Resp
{
error: result.error,
userId: user.id,
cid: body.cid,
},
'Failed to trigger referee recommendation',
);
Expand All @@ -54,13 +107,23 @@ export const triggerRecommendation = async (req: AuthenticatedRequest, res: Resp
logger.info(
{
userId: user.id,
uploadedFileName: responseData.uploaded_file_name,
uploadedFileName: responseData.UploadedFileName,
info: responseData.info,
},
'Successfully triggered referee recommendation',
);

return sendSuccess(res, responseData);
const formattedResponse = _.omit(responseData, 'execution_arn');

// Check if results are already available based on info message
const isResultReady =
responseData.info?.toLowerCase().includes('already exists') &&
responseData.info?.toLowerCase().includes('ready to be polled');

return sendSuccess(res, {
...formattedResponse,
cached: isResultReady,
});
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn({ error: error.errors }, 'Invalid request parameters');
Expand Down
Loading