Compare commits
2 Commits
backup/sta
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794fe29d8d | ||
|
|
0bade06255 |
@@ -18,8 +18,10 @@
|
||||
"test:onboarding-read": "tsx scripts/onboarding-rev10-read.test.ts",
|
||||
"test:interview-user-identity": "tsx scripts/interview-user-identity.test.ts",
|
||||
"test:ws-ticket": "tsx scripts/ws-ticket.test.ts",
|
||||
"test:roleplay-drafts": "tsx scripts/roleplay-drafts.test.ts",
|
||||
"test:artifact-transport": "tsx scripts/artifact-transport.test.ts",
|
||||
"test:resume-gateway": "tsx scripts/resume-gateway.test.ts",
|
||||
"test:roleplay-leaderboard": "tsx scripts/roleplay-leaderboard.test.ts",
|
||||
"test:interview-leaderboard": "tsx scripts/interview-leaderboard.test.ts",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"workflows:smoke": "tsx src/workflows/smoke-test.ts",
|
||||
|
||||
48
scripts/artifact-transport.test.ts
Normal file
48
scripts/artifact-transport.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { serviceRoutes } from "../src/routes/services.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: Request[] = [];
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
requests.push(request);
|
||||
const path = new URL(request.url).pathname;
|
||||
if (path.endsWith("/api/v1/artifacts/sess-1/session_video")) {
|
||||
return new Response(JSON.stringify({ url: "http://s3.test/obj", mime_type: "video/webm" }), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (path === "/obj") return new Response(new Uint8Array([1, 2, 3]), { headers: { "content-type": "video/webm", "content-length": "3" } });
|
||||
if (path.endsWith("/api/v1/sessions/sess-1/video/upload-url")) {
|
||||
return new Response(JSON.stringify({ url: "http://s3.test/put", mime_type: "video/webm" }), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (path === "/put") return new Response(null, { status: 200 });
|
||||
throw new Error(`unexpected upstream ${request.method} ${request.url}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const app = serviceRoutes({ skipAuth: true });
|
||||
try {
|
||||
for (const product of ["interview", "roleplay"] as const) {
|
||||
const download = await app.request(`http://backend.test/${product}/artifacts/sess-1/session_video/download`);
|
||||
assert.equal(download.status, 200);
|
||||
const downloadedBytes = new Uint8Array(await download.arrayBuffer());
|
||||
assert.deepEqual(Array.from(downloadedBytes), [1, 2, 3]);
|
||||
assert.equal(download.headers.get("content-type"), "video/webm");
|
||||
assert.equal(new TextDecoder().decode(downloadedBytes).includes("s3.test"), false);
|
||||
|
||||
const uploadBody = new Uint8Array([9, 8, 7]);
|
||||
const upload = await app.request(`http://backend.test/${product}/sessions/sess-1/video/upload`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "video/webm" },
|
||||
body: uploadBody,
|
||||
});
|
||||
assert.equal(upload.status, 204);
|
||||
const put = requests.at(-1)!;
|
||||
assert.equal(put.method, "PUT");
|
||||
assert.equal(put.headers.get("content-type"), "video/webm");
|
||||
assert.deepEqual(Array.from(new Uint8Array(await put.arrayBuffer())), [9, 8, 7]);
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
console.log("artifact transport gateway tests passed");
|
||||
32
scripts/deadline-hierarchy.test.ts
Normal file
32
scripts/deadline-hierarchy.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { INTERACTIVE_SERVICE_TIMEOUT_MS, serviceJson, roleplayService } from "../src/services/product-service-clients.js";
|
||||
|
||||
assert.equal(INTERACTIVE_SERVICE_TIMEOUT_MS, 240_000, "interactive service deadline must stay inside the 300s proxy budget");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalTimeout = AbortSignal.timeout;
|
||||
const requests: Request[] = [];
|
||||
let capturedTimeout = 0;
|
||||
AbortSignal.timeout = ((ms: number) => {
|
||||
capturedTimeout = ms;
|
||||
return new AbortController().signal;
|
||||
}) as typeof AbortSignal.timeout;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
requests.push(request);
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await roleplayService.plan("session-1", "user-1");
|
||||
assert.equal(new URL(requests[0]!.url).pathname, "/api/v1/roleplays/sessions/session-1/plan");
|
||||
assert.equal(capturedTimeout, INTERACTIVE_SERVICE_TIMEOUT_MS, "interactive plan calls must use the inner deadline");
|
||||
|
||||
requests.length = 0;
|
||||
capturedTimeout = 0;
|
||||
await serviceJson("http://service.test", "/slow", { timeoutMs: 17 });
|
||||
assert.equal(capturedTimeout, 17);
|
||||
} finally {
|
||||
AbortSignal.timeout = originalTimeout;
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
console.log("deadline hierarchy tests passed");
|
||||
39
scripts/interview-leaderboard.test.ts
Normal file
39
scripts/interview-leaderboard.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { serviceRoutes } from "../src/routes/services.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: Request[] = [];
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
requests.push(request);
|
||||
return new Response(JSON.stringify({ entries: [] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const app = serviceRoutes({ skipAuth: true });
|
||||
try {
|
||||
const leaderboard = await app.request("http://backend.test/interview/leaderboard?org_id=org-X&limit=100");
|
||||
assert.equal(leaderboard.status, 200);
|
||||
assert.deepEqual(await leaderboard.json(), { entries: [] });
|
||||
|
||||
const upstream = new URL(requests[0]!.url);
|
||||
assert.equal(upstream.pathname, "/api/v1/leaderboard");
|
||||
assert.equal(upstream.searchParams.get("limit"), "100");
|
||||
assert.equal(upstream.searchParams.get("org_id"), "org-X");
|
||||
assert.match(requests[0]!.headers.get("authorization") ?? "", /^Bearer /);
|
||||
|
||||
const clamped = await app.request("http://backend.test/interview/leaderboard?limit=101");
|
||||
assert.equal(clamped.status, 200);
|
||||
assert.equal(new URL(requests[1]!.url).searchParams.get("limit"), "100");
|
||||
|
||||
const defaulted = await app.request("http://backend.test/interview/leaderboard?limit=not-a-number");
|
||||
assert.equal(defaulted.status, 200);
|
||||
assert.equal(new URL(requests[2]!.url).searchParams.get("limit"), "10");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
console.log("interview leaderboard gateway: all assertions passed");
|
||||
@@ -12,9 +12,13 @@ globalThis.fetch = (async (input, init) => {
|
||||
? { drafts: [{ session_id: "draft-1" }] }
|
||||
: url.pathname.endsWith("/plan")
|
||||
? { session_id: "draft-1", status: "draft" }
|
||||
: url.pathname.endsWith("/configure/approve")
|
||||
? { session_id: "draft-1", status: "configured", approved: true }
|
||||
: { deleted: true, session_id: "draft-1" };
|
||||
: url.pathname.endsWith("/video-usage")
|
||||
? { used: 1, limit: 3, remaining: 2 }
|
||||
: url.pathname.endsWith("/vip-check")
|
||||
? { valid: true }
|
||||
: url.pathname.endsWith("/configure/approve")
|
||||
? { session_id: "draft-1", status: "configured", approved: true }
|
||||
: { deleted: true, session_id: "draft-1" };
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
@@ -42,13 +46,16 @@ try {
|
||||
assert.equal(deleted.status, 200);
|
||||
assert.deepEqual(await deleted.json(), { deleted: true, session_id: "draft-1" });
|
||||
|
||||
assert.equal(requests.length, 4);
|
||||
assert.match(requests[0]!.url, /\/api\/v1\/roleplays\/drafts\?user_id=user_test&limit=25$/);
|
||||
assert.match(requests[1]!.url, /\/api\/v1\/roleplays\/sessions\/draft-1\/plan$/);
|
||||
assert.match(requests[2]!.url, /\/api\/v1\/roleplays\/configure\/approve$/);
|
||||
assert.equal(requests[3]!.method, "DELETE");
|
||||
assert.match(requests[3]!.url, /\/api\/v1\/roleplays\/drafts\/draft-1$/);
|
||||
for (const request of requests) {
|
||||
const usage = await app.request("http://backend.test/roleplay/video-usage");
|
||||
assert.equal(usage.status, 200);
|
||||
assert.deepEqual(await usage.json(), { used: 1, limit: 3, remaining: 2 });
|
||||
const vip = await app.request("http://backend.test/roleplay/vip-check?code=grow26");
|
||||
assert.equal(vip.status, 200);
|
||||
assert.deepEqual(await vip.json(), { valid: true });
|
||||
assert.match(requests.find((request) => request.url.includes("/api/v1/roleplays/vip-check"))?.url ?? "", /code=grow26&user_id=user_test$/);
|
||||
assert.match(requests.find((request) => request.url.includes("/api/v1/roleplays/video-usage"))?.url ?? "", /\/api\/v1\/roleplays\/video-usage\?user_id=user_test$/);
|
||||
assert.ok(requests.length >= 5);
|
||||
for (const request of requests.filter((request) => request.url.includes("/api/v1/"))) {
|
||||
assert.equal(request.headers.get("x-growqr-user"), "user_test");
|
||||
assert.match(request.headers.get("authorization") ?? "", /^Bearer /);
|
||||
}
|
||||
|
||||
39
scripts/roleplay-leaderboard.test.ts
Normal file
39
scripts/roleplay-leaderboard.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { serviceRoutes } from "../src/routes/services.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: Request[] = [];
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
requests.push(request);
|
||||
return new Response(JSON.stringify({ entries: [] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const app = serviceRoutes({ skipAuth: true });
|
||||
try {
|
||||
const leaderboard = await app.request("http://backend.test/roleplay/leaderboard?org_id=org-X&limit=100");
|
||||
assert.equal(leaderboard.status, 200);
|
||||
assert.deepEqual(await leaderboard.json(), { entries: [] });
|
||||
|
||||
const upstream = new URL(requests[0]!.url);
|
||||
assert.equal(upstream.pathname, "/api/v1/roleplays/leaderboard");
|
||||
assert.equal(upstream.searchParams.get("limit"), "100");
|
||||
assert.equal(upstream.searchParams.get("org_id"), "org-X");
|
||||
assert.match(requests[0]!.headers.get("authorization") ?? "", /^Bearer /);
|
||||
|
||||
const clamped = await app.request("http://backend.test/roleplay/leaderboard?limit=101");
|
||||
assert.equal(clamped.status, 200);
|
||||
assert.equal(new URL(requests[1]!.url).searchParams.get("limit"), "100");
|
||||
|
||||
const defaulted = await app.request("http://backend.test/roleplay/leaderboard?limit=not-a-number");
|
||||
assert.equal(defaulted.status, 200);
|
||||
assert.equal(new URL(requests[2]!.url).searchParams.get("limit"), "10");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
console.log("roleplay leaderboard gateway: all assertions passed");
|
||||
@@ -788,6 +788,46 @@ async function proxySocialRequest(req: Request, rest: string, userId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function streamServiceArtifact(input: { service: "interview" | "roleplay"; sessionId: string; artifactType: string; userId: string; request: Request }) {
|
||||
const metadata = input.service === "interview"
|
||||
? await interviewService.artifact(input.sessionId, input.artifactType, input.userId)
|
||||
: await roleplayService.artifact(input.sessionId, input.artifactType, input.userId);
|
||||
const url = isRecord(metadata) ? getString(metadata.url) : undefined;
|
||||
if (!url) throw new HTTPException(502, { message: "artifact URL missing" });
|
||||
const headers = new Headers({ accept: input.request.headers.get("accept") ?? "*/*" });
|
||||
const range = input.request.headers.get("range");
|
||||
if (range) headers.set("range", range);
|
||||
const upstream = await fetch(url, { headers });
|
||||
if (!upstream.ok && upstream.status !== 206) {
|
||||
throw new HTTPException(upstream.status as never, { message: `artifact download failed: ${upstream.status}` });
|
||||
}
|
||||
const responseHeaders = new Headers();
|
||||
for (const name of ["content-type", "content-length", "content-range", "accept-ranges", "etag", "last-modified"]) {
|
||||
const value = upstream.headers.get(name);
|
||||
if (value) responseHeaders.set(name, value);
|
||||
}
|
||||
if (!responseHeaders.has("content-type") && isRecord(metadata) && typeof metadata.mime_type === "string") responseHeaders.set("content-type", metadata.mime_type);
|
||||
responseHeaders.set("content-disposition", `inline; filename="${input.artifactType}"`);
|
||||
responseHeaders.set("cache-control", "private, max-age=300");
|
||||
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
|
||||
}
|
||||
|
||||
async function proxyVideoUpload(input: { service: "interview" | "roleplay"; sessionId: string; userId: string; request: Request }) {
|
||||
const payload = input.service === "interview"
|
||||
? await interviewService.createVideoUploadUrl(input.sessionId, input.userId)
|
||||
: await roleplayService.createVideoUploadUrl(input.sessionId, input.userId);
|
||||
const url = isRecord(payload) ? getString(payload.url) : undefined;
|
||||
if (!url) throw new HTTPException(502, { message: "video upload URL missing" });
|
||||
const headers = new Headers();
|
||||
const contentType = input.request.headers.get("content-type");
|
||||
if (contentType) headers.set("content-type", contentType);
|
||||
const contentLength = input.request.headers.get("content-length");
|
||||
if (contentLength) headers.set("content-length", contentLength);
|
||||
const upstream = await fetch(url, { method: "PUT", headers, body: input.request.body, duplex: "half" } as RequestInit & { duplex: "half" });
|
||||
if (!upstream.ok) throw new HTTPException(upstream.status as never, { message: `video upload failed: ${upstream.status}` });
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
const app = new Hono<AuthContext>();
|
||||
if (!options.skipAuth) app.use("*", requireUser);
|
||||
@@ -915,6 +955,11 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
linkedin_available: hasLinkedInContext(userContext),
|
||||
});
|
||||
});
|
||||
app.get("/interview/sessions", async (c) => {
|
||||
const requested = Number(c.req.query("limit") ?? 100);
|
||||
const limit = Math.min(100, Math.max(1, Number.isFinite(requested) ? Math.trunc(requested) : 100));
|
||||
return c.json(await interviewService.listSessions(c.get("userId"), limit).catch(serviceErrorResponse));
|
||||
});
|
||||
app.post("/interview/configure", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const body = await c.req.json<JsonObject>();
|
||||
@@ -969,8 +1014,13 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
}).catch((err) => log.warn({ err }, "failed to record interview review event"));
|
||||
return c.json(result);
|
||||
});
|
||||
app.get("/interview/leaderboard", async (c) => c.json(await interviewService.leaderboard()));
|
||||
app.get("/interview/leaderboard", async (c) => {
|
||||
const requestedLimit = Number(c.req.query("limit"));
|
||||
const limit = Number.isInteger(requestedLimit) ? Math.min(100, Math.max(1, requestedLimit)) : 10;
|
||||
return c.json(await interviewService.leaderboard(c.req.query("org_id"), limit));
|
||||
});
|
||||
app.get("/interview/artifacts/:sessionId/:artifactType", async (c) => c.json(await interviewService.artifact(c.req.param("sessionId"), c.req.param("artifactType"), c.get("userId"))));
|
||||
app.get("/interview/artifacts/:sessionId/:artifactType/download", async (c) => streamServiceArtifact({ service: "interview", sessionId: c.req.param("sessionId"), artifactType: c.req.param("artifactType"), userId: c.get("userId"), request: c.req.raw }));
|
||||
app.post("/interview/sessions/:sessionId/ticket", async (c) => {
|
||||
if (!config.sessionTicketSecret) throw new HTTPException(503, { message: "session ticket signing is not configured" });
|
||||
const sessionId = c.req.param("sessionId");
|
||||
@@ -980,6 +1030,7 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
return c.json({ ticket: issued.ticket, expiresAt: issued.expiresAt });
|
||||
});
|
||||
app.post("/interview/sessions/:sessionId/video/upload-url", async (c) => c.json(await interviewService.createVideoUploadUrl(c.req.param("sessionId"), c.get("userId"))));
|
||||
app.put("/interview/sessions/:sessionId/video/upload", async (c) => proxyVideoUpload({ service: "interview", sessionId: c.req.param("sessionId"), userId: c.get("userId"), request: c.req.raw }));
|
||||
app.post("/interview/sessions/:sessionId/video/uploaded", async (c) => c.json(await interviewService.markVideoUploaded(c.req.param("sessionId"), c.get("userId"))));
|
||||
|
||||
app.get("/roleplay/page-state", async (c) => {
|
||||
@@ -997,6 +1048,11 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
linkedin_available: hasLinkedInContext(userContext),
|
||||
});
|
||||
});
|
||||
app.get("/roleplay/sessions", async (c) => {
|
||||
const requested = Number(c.req.query("limit") ?? 200);
|
||||
const limit = Math.min(200, Math.max(1, Number.isFinite(requested) ? Math.trunc(requested) : 200));
|
||||
return c.json(await roleplayService.listSessions(c.get("userId"), limit).catch(serviceErrorResponse));
|
||||
});
|
||||
app.post("/roleplay/configure", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const body = await c.req.json<JsonObject>();
|
||||
@@ -1045,6 +1101,10 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
const limit = Math.min(200, Math.max(1, Number(c.req.query("limit") ?? 50) || 50));
|
||||
return c.json(await roleplayService.drafts(c.get("userId"), limit).catch(serviceErrorResponse));
|
||||
});
|
||||
app.get("/roleplay/video-usage", async (c) =>
|
||||
c.json(await roleplayService.videoUsage(c.get("userId")).catch(serviceErrorResponse)));
|
||||
app.get("/roleplay/vip-check", async (c) =>
|
||||
c.json(await roleplayService.vipCheck(c.req.query("code") ?? "", c.get("userId")).catch(serviceErrorResponse)));
|
||||
app.delete("/roleplay/drafts/:sessionId", async (c) =>
|
||||
c.json(await roleplayService.deleteDraft(c.req.param("sessionId"), c.get("userId")).catch(serviceErrorResponse)));
|
||||
app.post("/roleplay/assignments", async (c) => c.json(await roleplayService.createAssignments(await c.req.json())));
|
||||
@@ -1069,8 +1129,13 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
}).catch((err) => log.warn({ err }, "failed to record roleplay review event"));
|
||||
return c.json(result);
|
||||
});
|
||||
app.get("/roleplay/leaderboard", async (c) => c.json(await roleplayService.leaderboard()));
|
||||
app.get("/roleplay/leaderboard", async (c) => {
|
||||
const requestedLimit = Number(c.req.query("limit"));
|
||||
const limit = Number.isInteger(requestedLimit) ? Math.min(100, Math.max(1, requestedLimit)) : 10;
|
||||
return c.json(await roleplayService.leaderboard(c.req.query("org_id"), limit));
|
||||
});
|
||||
app.get("/roleplay/artifacts/:sessionId/:artifactType", async (c) => c.json(await roleplayService.artifact(c.req.param("sessionId"), c.req.param("artifactType"), c.get("userId"))));
|
||||
app.get("/roleplay/artifacts/:sessionId/:artifactType/download", async (c) => streamServiceArtifact({ service: "roleplay", sessionId: c.req.param("sessionId"), artifactType: c.req.param("artifactType"), userId: c.get("userId"), request: c.req.raw }));
|
||||
app.post("/roleplay/sessions/:sessionId/ticket", async (c) => {
|
||||
if (!config.sessionTicketSecret) throw new HTTPException(503, { message: "session ticket signing is not configured" });
|
||||
const sessionId = c.req.param("sessionId");
|
||||
@@ -1080,6 +1145,7 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
||||
return c.json({ ticket: issued.ticket, expiresAt: issued.expiresAt });
|
||||
});
|
||||
app.post("/roleplay/sessions/:sessionId/video/upload-url", async (c) => c.json(await roleplayService.createVideoUploadUrl(c.req.param("sessionId"), c.get("userId"))));
|
||||
app.put("/roleplay/sessions/:sessionId/video/upload", async (c) => proxyVideoUpload({ service: "roleplay", sessionId: c.req.param("sessionId"), userId: c.get("userId"), request: c.req.raw }));
|
||||
app.post("/roleplay/sessions/:sessionId/video/uploaded", async (c) => c.json(await roleplayService.markVideoUploaded(c.req.param("sessionId"), c.get("userId"))));
|
||||
|
||||
app.get("/resume/state/:clerkId", async (c) => c.json(await resumeService.state(c.req.param("clerkId"))));
|
||||
|
||||
@@ -21,8 +21,8 @@ export class ProductServiceError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_SERVICE_TIMEOUT_MS = Number(process.env.PRODUCT_SERVICE_TIMEOUT_MS ?? 3500);
|
||||
const INTERACTIVE_SERVICE_TIMEOUT_MS = Number(process.env.PRODUCT_INTERACTIVE_SERVICE_TIMEOUT_MS ?? 120000);
|
||||
export const DEFAULT_SERVICE_TIMEOUT_MS = Number(process.env.PRODUCT_SERVICE_TIMEOUT_MS ?? 3500);
|
||||
export const INTERACTIVE_SERVICE_TIMEOUT_MS = Number(process.env.PRODUCT_INTERACTIVE_SERVICE_TIMEOUT_MS ?? 240000);
|
||||
|
||||
function userHeader(userId?: string): Record<string, string> | undefined {
|
||||
return userId ? { "x-growqr-user": userId } : undefined;
|
||||
@@ -60,6 +60,10 @@ export const interviewService = {
|
||||
serviceJson(config.interviewServiceUrl, `/api/v1/interviews/page-state?${new URLSearchParams({ user_id: userId })}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
listSessions: (userId: string, limit = 100) =>
|
||||
serviceJson(config.interviewServiceUrl, `/api/v1/sessions?${new URLSearchParams({ user_id: userId, limit: String(limit) })}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
configure: (payload: JsonObject, userId?: string) =>
|
||||
serviceJson(config.interviewServiceUrl, "/api/v1/configure", { body: payload, headers: userHeader(userId), timeoutMs: INTERACTIVE_SERVICE_TIMEOUT_MS }),
|
||||
preview: (payload: JsonObject, userId?: string) =>
|
||||
@@ -80,7 +84,11 @@ export const interviewService = {
|
||||
serviceJson(config.interviewServiceUrl, `/api/v1/review/${encodeURIComponent(sessionId)}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
leaderboard: () => serviceJson(config.interviewServiceUrl, "/api/v1/leaderboard"),
|
||||
leaderboard: (orgId?: string, limit = 10) =>
|
||||
serviceJson(config.interviewServiceUrl, `/api/v1/leaderboard?${new URLSearchParams({
|
||||
...(orgId ? { org_id: orgId } : {}),
|
||||
limit: String(limit),
|
||||
})}`),
|
||||
artifact: (sessionId: string, artifactType: string, userId?: string) =>
|
||||
serviceJson(config.interviewServiceUrl, `/api/v1/artifacts/${encodeURIComponent(sessionId)}/${encodeURIComponent(artifactType)}`, {
|
||||
headers: userHeader(userId),
|
||||
@@ -109,6 +117,10 @@ export const roleplayService = {
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/page-state?${new URLSearchParams({ user_id: userId })}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
listSessions: (userId: string, limit = 200) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/sessions?${new URLSearchParams({ user_id: userId, limit: String(limit) })}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
configure: (payload: JsonObject, userId?: string) => serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/configure", { body: payload, headers: userHeader(userId), timeoutMs: INTERACTIVE_SERVICE_TIMEOUT_MS }),
|
||||
preview: (payload: JsonObject, userId?: string) => serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/configure/preview", { body: payload, headers: userHeader(userId), timeoutMs: INTERACTIVE_SERVICE_TIMEOUT_MS }),
|
||||
editQuestions: (payload: { session_id: string; questions: Array<JsonObject | string> }, userId?: string) =>
|
||||
@@ -118,6 +130,7 @@ export const roleplayService = {
|
||||
plan: (sessionId: string, userId?: string) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/sessions/${encodeURIComponent(sessionId)}/plan`, {
|
||||
headers: userHeader(userId),
|
||||
timeoutMs: INTERACTIVE_SERVICE_TIMEOUT_MS,
|
||||
}),
|
||||
drafts: (userId: string, limit = 50) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/drafts?${new URLSearchParams({ user_id: userId, limit: String(limit) })}`, {
|
||||
@@ -140,7 +153,19 @@ export const roleplayService = {
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/review/${encodeURIComponent(sessionId)}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
leaderboard: () => serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/leaderboard"),
|
||||
leaderboard: (orgId?: string, limit = 10) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/leaderboard?${new URLSearchParams({
|
||||
...(orgId ? { org_id: orgId } : {}),
|
||||
limit: String(limit),
|
||||
})}`),
|
||||
videoUsage: (userId: string) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/video-usage?${new URLSearchParams({ user_id: userId })}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
vipCheck: (code: string, userId: string) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/vip-check?${new URLSearchParams({ code, user_id: userId })}`, {
|
||||
headers: userHeader(userId),
|
||||
}),
|
||||
artifact: (sessionId: string, artifactType: string, userId?: string) =>
|
||||
serviceJson(config.roleplayServiceUrl, `/api/v1/artifacts/${encodeURIComponent(sessionId)}/${encodeURIComponent(artifactType)}`, {
|
||||
headers: userHeader(userId),
|
||||
|
||||
Reference in New Issue
Block a user