@hadoku/jobplatform-worker 2.32.36 → 2.32.37
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/dist/index.js +575 -568
- package/dist/routes/jobs/detail.d.ts.map +1 -1
- package/dist/routes/jobs/pipeline.d.ts +15 -0
- package/dist/routes/jobs/pipeline.d.ts.map +1 -1
- package/dist/schemas.d.ts +24 -8
- package/dist/schemas.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -532,6 +532,14 @@ const JobSummarySchema = z.object({
|
|
|
532
532
|
vote: z.union([z.literal(1), z.literal(-1)]).nullable().optional(),
|
|
533
533
|
vote_reasons: z.array(z.string()).optional()
|
|
534
534
|
}).openapi("JobSummary");
|
|
535
|
+
const PrioritySchema = z.union([z.literal(1), z.literal(2), z.literal(3)]);
|
|
536
|
+
const TrackingSchema = z.object({
|
|
537
|
+
notes: z.string().nullable(),
|
|
538
|
+
priority: PrioritySchema.nullable(),
|
|
539
|
+
follow_up_at: z.string().nullable(),
|
|
540
|
+
contact_name: z.string().nullable(),
|
|
541
|
+
contact_email: z.string().nullable()
|
|
542
|
+
}).openapi("Tracking");
|
|
535
543
|
const JobDetailSchema = JobSummarySchema.extend({
|
|
536
544
|
/**
|
|
537
545
|
* Whether the form runner has an adapter for this posting. A PREDICTION
|
|
@@ -562,7 +570,9 @@ const JobDetailSchema = JobSummarySchema.extend({
|
|
|
562
570
|
department: z.string().nullable(),
|
|
563
571
|
scraper_used: z.string().nullable(),
|
|
564
572
|
run_id: z.string().nullable(),
|
|
565
|
-
state_updated_at: z.string().nullable()
|
|
573
|
+
state_updated_at: z.string().nullable(),
|
|
574
|
+
/** V5 tracking; null when the caller has no state for this posting (or is anonymous). */
|
|
575
|
+
tracking: TrackingSchema.nullable()
|
|
566
576
|
}).openapi("JobDetail");
|
|
567
577
|
const FEEDBACK_REASONS = [
|
|
568
578
|
// downvote-flavored
|
|
@@ -998,7 +1008,6 @@ const AddJobEventSchema = z.object({
|
|
|
998
1008
|
/** When it happened; defaults to now. */
|
|
999
1009
|
occurred_at: WhenSchema.optional()
|
|
1000
1010
|
}).openapi("AddJobEvent");
|
|
1001
|
-
const PrioritySchema = z.union([z.literal(1), z.literal(2), z.literal(3)]);
|
|
1002
1011
|
const SetTrackingSchema = z.object({
|
|
1003
1012
|
notes: z.string().max(NOTE_MAX).nullable().optional(),
|
|
1004
1013
|
priority: PrioritySchema.nullable().optional(),
|
|
@@ -1008,13 +1017,6 @@ const SetTrackingSchema = z.object({
|
|
|
1008
1017
|
}).refine((o) => Object.values(o).some((v) => v !== void 0), {
|
|
1009
1018
|
message: "name at least one field to change"
|
|
1010
1019
|
}).openapi("SetTracking");
|
|
1011
|
-
const TrackingSchema = z.object({
|
|
1012
|
-
notes: z.string().nullable(),
|
|
1013
|
-
priority: PrioritySchema.nullable(),
|
|
1014
|
-
follow_up_at: z.string().nullable(),
|
|
1015
|
-
contact_name: z.string().nullable(),
|
|
1016
|
-
contact_email: z.string().nullable()
|
|
1017
|
-
}).openapi("Tracking");
|
|
1018
1020
|
const TrackingResponseSchema = S(
|
|
1019
1021
|
z.object({ job_id: z.string(), tracking: TrackingSchema })
|
|
1020
1022
|
).openapi("TrackingResponse");
|
|
@@ -4425,319 +4427,524 @@ function applyTier(applicationUrl, ats) {
|
|
|
4425
4427
|
if (KNOWN_UNSUPPORTED.some((h) => host === h || host.endsWith(`.${h}`))) return "unsupported";
|
|
4426
4428
|
return "unknown";
|
|
4427
4429
|
}
|
|
4428
|
-
|
|
4430
|
+
const IDENTITY_ERRORS = {
|
|
4431
|
+
404: {
|
|
4432
|
+
description: "No such owner name, or no such job",
|
|
4433
|
+
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
4434
|
+
},
|
|
4435
|
+
409: {
|
|
4436
|
+
description: "That owner name has never signed in, or the posting is not in the pipeline",
|
|
4437
|
+
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
4438
|
+
},
|
|
4439
|
+
503: {
|
|
4440
|
+
description: "Identity could not be resolved right now — retry",
|
|
4441
|
+
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
4442
|
+
}
|
|
4443
|
+
};
|
|
4444
|
+
const FORBIDDEN$1 = {
|
|
4445
|
+
description: "Forbidden",
|
|
4446
|
+
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
4447
|
+
};
|
|
4448
|
+
const EVENT_COLS = "id, job_id, kind, from_state, to_state, note, occurred_at, created_at";
|
|
4449
|
+
const TRACKING_COLS = "notes, priority, follow_up_at, contact_name, contact_email";
|
|
4450
|
+
function stateOrNull(v) {
|
|
4451
|
+
const s = asJobStateRead(v);
|
|
4452
|
+
return s === "new" ? null : s;
|
|
4453
|
+
}
|
|
4454
|
+
function toEvent(r) {
|
|
4455
|
+
return {
|
|
4456
|
+
...r,
|
|
4457
|
+
kind: r.kind,
|
|
4458
|
+
from_state: stateOrNull(r.from_state),
|
|
4459
|
+
to_state: stateOrNull(r.to_state)
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
function asPriority(p) {
|
|
4463
|
+
return p === 1 || p === 2 || p === 3 ? p : null;
|
|
4464
|
+
}
|
|
4465
|
+
function toTracking(r) {
|
|
4466
|
+
return {
|
|
4467
|
+
notes: r.notes,
|
|
4468
|
+
priority: asPriority(r.priority),
|
|
4469
|
+
follow_up_at: r.follow_up_at,
|
|
4470
|
+
contact_name: r.contact_name,
|
|
4471
|
+
contact_email: r.contact_email
|
|
4472
|
+
};
|
|
4473
|
+
}
|
|
4474
|
+
function notInPipeline(id) {
|
|
4475
|
+
return {
|
|
4476
|
+
success: false,
|
|
4477
|
+
error: "Conflict",
|
|
4478
|
+
message: `Job '${id}' is not in your pipeline — set its state first (PUT /jobs/${id}/state)`
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
function jobNotFound(id) {
|
|
4482
|
+
return { success: false, error: "Not found", message: `Job '${id}' not found` };
|
|
4483
|
+
}
|
|
4484
|
+
async function stateOf(db, jobId, userId) {
|
|
4485
|
+
const row = await db.prepare(
|
|
4486
|
+
`SELECT j.id, js.state FROM jobs j
|
|
4487
|
+
LEFT JOIN job_states js ON js.job_id = j.id AND js.user_id = ?
|
|
4488
|
+
WHERE j.id = ?`
|
|
4489
|
+
).bind(userId, jobId).first();
|
|
4490
|
+
if (!row) return "missing";
|
|
4491
|
+
return row.state ? "tracked" : "untracked";
|
|
4492
|
+
}
|
|
4493
|
+
function registerPipelineRoutes(app2) {
|
|
4494
|
+
app2.get("/pipeline", gateAuthed);
|
|
4495
|
+
app2.patch("/jobs/:id/tracking", gateAuthed);
|
|
4496
|
+
app2.get("/jobs/:id/events", gateAuthed);
|
|
4497
|
+
app2.post("/jobs/:id/events", gateAuthed);
|
|
4498
|
+
app2.delete("/jobs/:id/events/:eventId", gateAuthed);
|
|
4429
4499
|
app2.openapi(
|
|
4430
4500
|
createRoute({
|
|
4431
4501
|
method: "get",
|
|
4432
|
-
path: "/
|
|
4433
|
-
tags: ["
|
|
4434
|
-
summary: "
|
|
4502
|
+
path: "/pipeline",
|
|
4503
|
+
tags: ["Pipeline"],
|
|
4504
|
+
summary: "Every posting in the caller's pipeline, most recently changed first",
|
|
4435
4505
|
request: {
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4506
|
+
query: ownerNameQuery.extend({
|
|
4507
|
+
include_dismissed: z.enum(["true", "false"]).optional().openapi({
|
|
4508
|
+
description: "Dismissed postings are left out unless this is true."
|
|
4509
|
+
})
|
|
4439
4510
|
})
|
|
4440
4511
|
},
|
|
4441
4512
|
responses: {
|
|
4442
4513
|
200: {
|
|
4443
|
-
description: "
|
|
4444
|
-
content: { "application/json": { schema:
|
|
4514
|
+
description: "Pipeline",
|
|
4515
|
+
content: { "application/json": { schema: PipelineResponseSchema } }
|
|
4445
4516
|
},
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4449
|
-
}
|
|
4517
|
+
403: FORBIDDEN$1,
|
|
4518
|
+
...IDENTITY_ERRORS
|
|
4450
4519
|
}
|
|
4451
4520
|
}),
|
|
4452
4521
|
async (c) => {
|
|
4453
|
-
const {
|
|
4454
|
-
const
|
|
4455
|
-
|
|
4456
|
-
const
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4522
|
+
const { ownerName, include_dismissed } = c.req.valid("query");
|
|
4523
|
+
const who = await effectiveUserId(c, ownerName);
|
|
4524
|
+
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
4525
|
+
const dismissed = include_dismissed === "true" ? "" : "AND js.state != 'dismissed'";
|
|
4526
|
+
const rows = await c.env.JOB_PLATFORM_DB.prepare(
|
|
4527
|
+
`SELECT js.job_id, j.title, j.company, j.location, j.workplace_type,
|
|
4528
|
+
j.salary_min, j.salary_max, j.url, j.posted_date,
|
|
4529
|
+
js.state, js.updated_at, js.variant_slug,
|
|
4530
|
+
js.notes, js.priority, js.follow_up_at, js.contact_name, js.contact_email,
|
|
4531
|
+
ap.status AS application_status,
|
|
4532
|
+
(SELECT MIN(e.occurred_at) FROM job_events e
|
|
4533
|
+
WHERE e.job_id = js.job_id AND e.user_id = js.user_id
|
|
4534
|
+
AND e.kind = 'state' AND e.to_state = 'applied') AS applied_at,
|
|
4535
|
+
(SELECT MAX(e.occurred_at) FROM job_events e
|
|
4536
|
+
WHERE e.job_id = js.job_id AND e.user_id = js.user_id) AS last_event_at
|
|
4537
|
+
FROM job_states js
|
|
4538
|
+
JOIN jobs j ON j.id = js.job_id
|
|
4539
|
+
LEFT JOIN applications ap ON ap.job_id = js.job_id AND ap.user_id = js.user_id
|
|
4540
|
+
WHERE js.user_id = ? ${dismissed}
|
|
4541
|
+
ORDER BY js.updated_at DESC`
|
|
4542
|
+
).bind(who.userId).all();
|
|
4543
|
+
const items = rows.results.flatMap((r) => {
|
|
4544
|
+
const state = stateOrNull(r.state);
|
|
4545
|
+
if (!state) return [];
|
|
4546
|
+
return [
|
|
4469
4547
|
{
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
// index and walked the whole board — 632 rows for toast,
|
|
4491
|
-
// 4,151 for anduril — to find usually nothing: ~3M rows a
|
|
4492
|
-
// day on 2026-10-15, from the apply runner's 5-minute
|
|
4493
|
-
// detail reads. Outer applications reads only the
|
|
4494
|
-
// caller's own rows (zero for a service key).
|
|
4495
|
-
`SELECT 1 FROM applications a
|
|
4496
|
-
CROSS JOIN jobs j ON j.id = a.job_id
|
|
4497
|
-
WHERE a.user_id = ? AND j.ats = ? AND j.slug = ?
|
|
4498
|
-
AND a.status IN ('filled', 'approved', 'submitted')
|
|
4499
|
-
LIMIT 1`
|
|
4500
|
-
).bind(userId, row.ats, row.slug).first();
|
|
4501
|
-
applyVerified = proven !== null;
|
|
4502
|
-
}
|
|
4503
|
-
let applicationStatus = null;
|
|
4504
|
-
if (userId) {
|
|
4505
|
-
const [stateRes, appRes] = await db.batch([
|
|
4506
|
-
db.prepare("SELECT state, updated_at FROM job_states WHERE job_id = ? AND user_id = ?").bind(id, userId),
|
|
4507
|
-
db.prepare("SELECT status FROM applications WHERE job_id = ? AND user_id = ?").bind(id, userId)
|
|
4508
|
-
]);
|
|
4509
|
-
const stateRow = stateRes.results[0];
|
|
4510
|
-
state = asJobStateRead(stateRow?.state);
|
|
4511
|
-
stateUpdatedAt = stateRow?.updated_at ?? null;
|
|
4512
|
-
const appRow = appRes.results[0];
|
|
4513
|
-
applicationStatus = appRow?.status ?? null;
|
|
4514
|
-
}
|
|
4515
|
-
const job = {
|
|
4516
|
-
id: row.id,
|
|
4517
|
-
title: row.title,
|
|
4518
|
-
company: row.company,
|
|
4519
|
-
location: row.location,
|
|
4520
|
-
workplace_type: row.workplace_type,
|
|
4521
|
-
job_type: row.job_type,
|
|
4522
|
-
salary_min: row.salary_min,
|
|
4523
|
-
salary_max: row.salary_max,
|
|
4524
|
-
source_site: row.source_site,
|
|
4525
|
-
url: row.url,
|
|
4526
|
-
posted_date: row.posted_date,
|
|
4527
|
-
scraped_at: row.scraped_at,
|
|
4528
|
-
ats: row.ats ?? null,
|
|
4529
|
-
slug: row.slug ?? null,
|
|
4530
|
-
slug_source: asSlugSource(row.slug_source ?? null),
|
|
4531
|
-
apply_tier: applyTier(
|
|
4532
|
-
row.application_url ?? row.url,
|
|
4533
|
-
row.ats ?? null
|
|
4534
|
-
),
|
|
4535
|
-
apply_verified: applyVerified,
|
|
4536
|
-
role_track: asRoleTrack(row.role_track ?? "unknown"),
|
|
4537
|
-
role_level: roleLevel,
|
|
4538
|
-
description: row.description,
|
|
4539
|
-
application_url: row.application_url,
|
|
4540
|
-
department: row.department,
|
|
4541
|
-
scraper_used: row.scraper_used,
|
|
4542
|
-
run_id: row.run_id,
|
|
4543
|
-
score,
|
|
4544
|
-
score_breakdown,
|
|
4545
|
-
state,
|
|
4546
|
-
application_status: asApplicationStatus(applicationStatus),
|
|
4547
|
-
state_updated_at: stateUpdatedAt
|
|
4548
|
-
};
|
|
4549
|
-
return c.json({ success: true, data: { job } }, 200);
|
|
4548
|
+
job_id: r.job_id,
|
|
4549
|
+
title: r.title,
|
|
4550
|
+
company: r.company,
|
|
4551
|
+
location: r.location,
|
|
4552
|
+
workplace_type: r.workplace_type,
|
|
4553
|
+
salary_min: r.salary_min,
|
|
4554
|
+
salary_max: r.salary_max,
|
|
4555
|
+
url: r.url,
|
|
4556
|
+
posted_date: r.posted_date,
|
|
4557
|
+
state,
|
|
4558
|
+
updated_at: r.updated_at,
|
|
4559
|
+
tracking: toTracking(r),
|
|
4560
|
+
variant_slug: r.variant_slug,
|
|
4561
|
+
application_status: asApplicationStatus(r.application_status),
|
|
4562
|
+
applied_at: r.applied_at,
|
|
4563
|
+
last_event_at: r.last_event_at
|
|
4564
|
+
}
|
|
4565
|
+
];
|
|
4566
|
+
});
|
|
4567
|
+
return c.json({ success: true, data: { items } }, 200);
|
|
4550
4568
|
}
|
|
4551
4569
|
);
|
|
4552
|
-
}
|
|
4553
|
-
function registerStateRoutes(app2) {
|
|
4554
|
-
app2.put("/jobs/:id/state", gateAuthed);
|
|
4555
|
-
app2.delete("/jobs/:id/state", gateAuthed);
|
|
4556
4570
|
app2.openapi(
|
|
4557
4571
|
createRoute({
|
|
4558
|
-
method: "
|
|
4559
|
-
path: "/jobs/{id}/
|
|
4560
|
-
tags: ["
|
|
4561
|
-
summary: "
|
|
4562
|
-
request: {
|
|
4572
|
+
method: "patch",
|
|
4573
|
+
path: "/jobs/{id}/tracking",
|
|
4574
|
+
tags: ["Pipeline"],
|
|
4575
|
+
summary: "Change what the caller tracks about one application; null clears a field",
|
|
4576
|
+
request: {
|
|
4563
4577
|
params: z.object({ id: z.string() }),
|
|
4564
|
-
|
|
4578
|
+
query: ownerNameQuery,
|
|
4579
|
+
body: { content: { "application/json": { schema: SetTrackingSchema } } }
|
|
4565
4580
|
},
|
|
4566
4581
|
responses: {
|
|
4567
4582
|
200: {
|
|
4568
|
-
description: "
|
|
4569
|
-
content: { "application/json": { schema:
|
|
4570
|
-
},
|
|
4571
|
-
403: {
|
|
4572
|
-
description: "Forbidden",
|
|
4573
|
-
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4583
|
+
description: "Tracking after the change",
|
|
4584
|
+
content: { "application/json": { schema: TrackingResponseSchema } }
|
|
4574
4585
|
},
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4578
|
-
}
|
|
4586
|
+
403: FORBIDDEN$1,
|
|
4587
|
+
...IDENTITY_ERRORS
|
|
4579
4588
|
}
|
|
4580
4589
|
}),
|
|
4581
4590
|
async (c) => {
|
|
4582
|
-
const userId = await maybeUserId(c);
|
|
4583
|
-
if (!userId) {
|
|
4584
|
-
return c.json(
|
|
4585
|
-
{ success: false, error: "Forbidden", message: "Authentication required" },
|
|
4586
|
-
403
|
|
4587
|
-
);
|
|
4588
|
-
}
|
|
4589
4591
|
const { id } = c.req.valid("param");
|
|
4592
|
+
const { ownerName } = c.req.valid("query");
|
|
4590
4593
|
const body = c.req.valid("json");
|
|
4594
|
+
const who = await effectiveUserId(c, ownerName);
|
|
4595
|
+
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
4591
4596
|
const db = c.env.JOB_PLATFORM_DB;
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4597
|
+
const sets = [];
|
|
4598
|
+
const binds = [];
|
|
4599
|
+
for (const col of [
|
|
4600
|
+
"notes",
|
|
4601
|
+
"priority",
|
|
4602
|
+
"follow_up_at",
|
|
4603
|
+
"contact_name",
|
|
4604
|
+
"contact_email"
|
|
4605
|
+
]) {
|
|
4606
|
+
const v = body[col];
|
|
4607
|
+
if (v === void 0) continue;
|
|
4608
|
+
sets.push(`${col} = ?`);
|
|
4609
|
+
binds.push(typeof v === "string" && v.trim() === "" ? null : v);
|
|
4602
4610
|
}
|
|
4603
4611
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4604
|
-
const
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
).bind(id, userId, body.state, now, variantSlug);
|
|
4613
|
-
if (exists.prev === body.state) {
|
|
4614
|
-
await upsert.run();
|
|
4615
|
-
} else {
|
|
4616
|
-
await db.batch([
|
|
4617
|
-
upsert,
|
|
4618
|
-
db.prepare(
|
|
4619
|
-
`INSERT INTO job_events
|
|
4620
|
-
(job_id, user_id, kind, from_state, to_state, note, occurred_at, created_at)
|
|
4621
|
-
VALUES (?, ?, 'state', ?, ?, NULL, ?, ?)`
|
|
4622
|
-
).bind(id, userId, exists.prev, body.state, now, now)
|
|
4623
|
-
]);
|
|
4612
|
+
const row = await db.prepare(
|
|
4613
|
+
`UPDATE job_states SET ${sets.join(", ")}, updated_at = ?
|
|
4614
|
+
WHERE job_id = ? AND user_id = ?
|
|
4615
|
+
RETURNING ${TRACKING_COLS}`
|
|
4616
|
+
).bind(...binds, now, id, who.userId).first();
|
|
4617
|
+
if (!row) {
|
|
4618
|
+
const s = await stateOf(db, id, who.userId);
|
|
4619
|
+
return s === "missing" ? c.json(jobNotFound(id), 404) : c.json(notInPipeline(id), 409);
|
|
4624
4620
|
}
|
|
4625
4621
|
return c.json(
|
|
4626
|
-
{
|
|
4627
|
-
success: true,
|
|
4628
|
-
data: { job_id: id, state: body.state, updated_at: now }
|
|
4629
|
-
},
|
|
4622
|
+
{ success: true, data: { job_id: id, tracking: toTracking(row) } },
|
|
4630
4623
|
200
|
|
4631
4624
|
);
|
|
4632
4625
|
}
|
|
4633
4626
|
);
|
|
4627
|
+
app2.openapi(
|
|
4628
|
+
createRoute({
|
|
4629
|
+
method: "get",
|
|
4630
|
+
path: "/jobs/{id}/events",
|
|
4631
|
+
tags: ["Pipeline"],
|
|
4632
|
+
summary: "One posting's timeline for the caller, oldest first",
|
|
4633
|
+
request: { params: z.object({ id: z.string() }), query: ownerNameQuery },
|
|
4634
|
+
responses: {
|
|
4635
|
+
200: {
|
|
4636
|
+
description: "Timeline (empty for a posting with none)",
|
|
4637
|
+
content: { "application/json": { schema: JobEventsResponseSchema } }
|
|
4638
|
+
},
|
|
4639
|
+
403: FORBIDDEN$1,
|
|
4640
|
+
...IDENTITY_ERRORS
|
|
4641
|
+
}
|
|
4642
|
+
}),
|
|
4643
|
+
async (c) => {
|
|
4644
|
+
const { id } = c.req.valid("param");
|
|
4645
|
+
const { ownerName } = c.req.valid("query");
|
|
4646
|
+
const who = await effectiveUserId(c, ownerName);
|
|
4647
|
+
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
4648
|
+
const rows = await c.env.JOB_PLATFORM_DB.prepare(
|
|
4649
|
+
`SELECT ${EVENT_COLS} FROM job_events
|
|
4650
|
+
WHERE job_id = ? AND user_id = ?
|
|
4651
|
+
ORDER BY occurred_at, id`
|
|
4652
|
+
).bind(id, who.userId).all();
|
|
4653
|
+
return c.json({ success: true, data: { events: rows.results.map(toEvent) } }, 200);
|
|
4654
|
+
}
|
|
4655
|
+
);
|
|
4656
|
+
app2.openapi(
|
|
4657
|
+
createRoute({
|
|
4658
|
+
method: "post",
|
|
4659
|
+
path: "/jobs/{id}/events",
|
|
4660
|
+
tags: ["Pipeline"],
|
|
4661
|
+
summary: "Log something that happened on one application",
|
|
4662
|
+
request: {
|
|
4663
|
+
params: z.object({ id: z.string() }),
|
|
4664
|
+
query: ownerNameQuery,
|
|
4665
|
+
body: { content: { "application/json": { schema: AddJobEventSchema } } }
|
|
4666
|
+
},
|
|
4667
|
+
responses: {
|
|
4668
|
+
201: {
|
|
4669
|
+
description: "Logged",
|
|
4670
|
+
content: { "application/json": { schema: JobEventResponseSchema } }
|
|
4671
|
+
},
|
|
4672
|
+
403: FORBIDDEN$1,
|
|
4673
|
+
...IDENTITY_ERRORS
|
|
4674
|
+
}
|
|
4675
|
+
}),
|
|
4676
|
+
async (c) => {
|
|
4677
|
+
const { id } = c.req.valid("param");
|
|
4678
|
+
const { ownerName } = c.req.valid("query");
|
|
4679
|
+
const body = c.req.valid("json");
|
|
4680
|
+
const who = await effectiveUserId(c, ownerName);
|
|
4681
|
+
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
4682
|
+
const db = c.env.JOB_PLATFORM_DB;
|
|
4683
|
+
const s = await stateOf(db, id, who.userId);
|
|
4684
|
+
if (s === "missing") return c.json(jobNotFound(id), 404);
|
|
4685
|
+
if (s === "untracked") return c.json(notInPipeline(id), 409);
|
|
4686
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4687
|
+
const occurred = body.occurred_at ? new Date(
|
|
4688
|
+
body.occurred_at.length === 10 ? `${body.occurred_at}T00:00:00Z` : body.occurred_at
|
|
4689
|
+
).toISOString() : now;
|
|
4690
|
+
const row = await db.prepare(
|
|
4691
|
+
`INSERT INTO job_events
|
|
4692
|
+
(job_id, user_id, kind, from_state, to_state, note, occurred_at, created_at)
|
|
4693
|
+
VALUES (?, ?, ?, NULL, NULL, ?, ?, ?)
|
|
4694
|
+
RETURNING ${EVENT_COLS}`
|
|
4695
|
+
).bind(id, who.userId, body.kind, body.note, occurred, now).first();
|
|
4696
|
+
if (!row) throw new Error("job_events INSERT … RETURNING returned no row");
|
|
4697
|
+
return c.json({ success: true, data: { event: toEvent(row) } }, 201);
|
|
4698
|
+
}
|
|
4699
|
+
);
|
|
4634
4700
|
app2.openapi(
|
|
4635
4701
|
createRoute({
|
|
4636
4702
|
method: "delete",
|
|
4637
|
-
path: "/jobs/{id}/
|
|
4638
|
-
tags: ["
|
|
4639
|
-
summary:
|
|
4703
|
+
path: "/jobs/{id}/events/{eventId}",
|
|
4704
|
+
tags: ["Pipeline"],
|
|
4705
|
+
summary: "Remove a note/interview/contact/follow-up the caller logged",
|
|
4640
4706
|
request: {
|
|
4641
|
-
params: z.object({ id: z.string() })
|
|
4707
|
+
params: z.object({ id: z.string(), eventId: z.coerce.number().int().positive() }),
|
|
4708
|
+
query: ownerNameQuery
|
|
4642
4709
|
},
|
|
4643
4710
|
responses: {
|
|
4644
4711
|
200: {
|
|
4645
|
-
description: "
|
|
4712
|
+
description: "Removed (or already absent)",
|
|
4646
4713
|
content: {
|
|
4647
4714
|
"application/json": {
|
|
4648
4715
|
schema: z.object({
|
|
4649
4716
|
success: z.literal(true),
|
|
4650
|
-
data: z.object({
|
|
4651
|
-
}).openapi("
|
|
4717
|
+
data: z.object({ id: z.number().int(), deleted: z.boolean() })
|
|
4718
|
+
}).openapi("DeleteJobEventResponse")
|
|
4652
4719
|
}
|
|
4653
4720
|
}
|
|
4654
4721
|
},
|
|
4655
|
-
403:
|
|
4656
|
-
|
|
4657
|
-
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4658
|
-
}
|
|
4722
|
+
403: FORBIDDEN$1,
|
|
4723
|
+
...IDENTITY_ERRORS
|
|
4659
4724
|
}
|
|
4660
4725
|
}),
|
|
4661
4726
|
async (c) => {
|
|
4662
|
-
const
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
]);
|
|
4675
|
-
const deleted = (result.meta?.changes ?? 0) > 0;
|
|
4676
|
-
return c.json({ success: true, data: { job_id: id, deleted } }, 200);
|
|
4727
|
+
const { id, eventId } = c.req.valid("param");
|
|
4728
|
+
const { ownerName } = c.req.valid("query");
|
|
4729
|
+
const who = await effectiveUserId(c, ownerName);
|
|
4730
|
+
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
4731
|
+
const res = await c.env.JOB_PLATFORM_DB.prepare(
|
|
4732
|
+
`DELETE FROM job_events
|
|
4733
|
+
WHERE id = ? AND job_id = ? AND user_id = ? AND kind != 'state'`
|
|
4734
|
+
).bind(eventId, id, who.userId).run();
|
|
4735
|
+
return c.json(
|
|
4736
|
+
{ success: true, data: { id: eventId, deleted: (res.meta?.changes ?? 0) > 0 } },
|
|
4737
|
+
200
|
|
4738
|
+
);
|
|
4677
4739
|
}
|
|
4678
4740
|
);
|
|
4679
4741
|
}
|
|
4680
|
-
function
|
|
4681
|
-
app2.put("/jobs/:id/feedback", gateAuthed);
|
|
4682
|
-
app2.delete("/jobs/:id/feedback", gateAuthed);
|
|
4742
|
+
function registerDetailRoute(app2) {
|
|
4683
4743
|
app2.openapi(
|
|
4684
4744
|
createRoute({
|
|
4685
|
-
method: "
|
|
4686
|
-
path: "/jobs/{id}
|
|
4745
|
+
method: "get",
|
|
4746
|
+
path: "/jobs/{id}",
|
|
4687
4747
|
tags: ["Jobs"],
|
|
4688
|
-
summary: "
|
|
4748
|
+
summary: "Get full job detail",
|
|
4689
4749
|
request: {
|
|
4690
4750
|
params: z.object({ id: z.string() }),
|
|
4691
|
-
|
|
4751
|
+
query: z.object({
|
|
4752
|
+
profile_id: z.string().optional().openapi({ description: "Include score for this profile" })
|
|
4753
|
+
})
|
|
4692
4754
|
},
|
|
4693
4755
|
responses: {
|
|
4694
4756
|
200: {
|
|
4695
|
-
description: "
|
|
4696
|
-
content: { "application/json": { schema:
|
|
4697
|
-
},
|
|
4698
|
-
403: {
|
|
4699
|
-
description: "Forbidden",
|
|
4700
|
-
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4757
|
+
description: "Job detail",
|
|
4758
|
+
content: { "application/json": { schema: JobResponseSchema } }
|
|
4701
4759
|
},
|
|
4702
4760
|
404: {
|
|
4703
|
-
description: "
|
|
4761
|
+
description: "Not found",
|
|
4704
4762
|
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4705
4763
|
}
|
|
4706
4764
|
}
|
|
4707
4765
|
}),
|
|
4708
4766
|
async (c) => {
|
|
4709
|
-
const userId = await maybeUserId(c);
|
|
4710
|
-
if (!userId) {
|
|
4711
|
-
return c.json(
|
|
4712
|
-
{ success: false, error: "Forbidden", message: "Authentication required" },
|
|
4713
|
-
403
|
|
4714
|
-
);
|
|
4715
|
-
}
|
|
4716
4767
|
const { id } = c.req.valid("param");
|
|
4717
|
-
const
|
|
4768
|
+
const { profile_id } = c.req.valid("query");
|
|
4718
4769
|
const db = c.env.JOB_PLATFORM_DB;
|
|
4719
|
-
const
|
|
4720
|
-
if (!
|
|
4770
|
+
const row = await db.prepare("SELECT * FROM jobs WHERE id = ?").bind(id).first();
|
|
4771
|
+
if (!row) {
|
|
4721
4772
|
return c.json(
|
|
4722
4773
|
{ success: false, error: "Not found", message: `Job '${id}' not found` },
|
|
4723
4774
|
404
|
|
4724
4775
|
);
|
|
4725
4776
|
}
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4777
|
+
let score = 0;
|
|
4778
|
+
let score_breakdown = { ...ZERO_BREAKDOWN };
|
|
4779
|
+
const roleLevel = asRoleLevel(row.role_level ?? null);
|
|
4780
|
+
if (profile_id) {
|
|
4781
|
+
const profile = await loadScorableProfile(db, profile_id);
|
|
4782
|
+
const result = scoreJob(
|
|
4783
|
+
{
|
|
4784
|
+
title: row.title,
|
|
4785
|
+
description: row.description,
|
|
4786
|
+
location: row.location ?? null,
|
|
4787
|
+
workplace_type: row.workplace_type,
|
|
4788
|
+
salary_max: row.salary_max ?? null,
|
|
4789
|
+
role_level: roleLevel
|
|
4790
|
+
},
|
|
4791
|
+
profile
|
|
4792
|
+
);
|
|
4793
|
+
score = result.score;
|
|
4794
|
+
score_breakdown = result.breakdown;
|
|
4795
|
+
}
|
|
4796
|
+
const userId = await maybeUserId(c);
|
|
4797
|
+
let state = null;
|
|
4798
|
+
let stateUpdatedAt = null;
|
|
4799
|
+
let tracking = null;
|
|
4800
|
+
let applyVerified = false;
|
|
4801
|
+
if (userId && row.ats && row.slug) {
|
|
4802
|
+
const proven = await db.prepare(
|
|
4803
|
+
// CROSS JOIN pins the caller's applications as the outer
|
|
4804
|
+
// loop. Left to choose, SQLite led with jobs' (ats, slug)
|
|
4805
|
+
// index and walked the whole board — 632 rows for toast,
|
|
4806
|
+
// 4,151 for anduril — to find usually nothing: ~3M rows a
|
|
4807
|
+
// day on 2026-10-15, from the apply runner's 5-minute
|
|
4808
|
+
// detail reads. Outer applications reads only the
|
|
4809
|
+
// caller's own rows (zero for a service key).
|
|
4810
|
+
`SELECT 1 FROM applications a
|
|
4811
|
+
CROSS JOIN jobs j ON j.id = a.job_id
|
|
4812
|
+
WHERE a.user_id = ? AND j.ats = ? AND j.slug = ?
|
|
4813
|
+
AND a.status IN ('filled', 'approved', 'submitted')
|
|
4814
|
+
LIMIT 1`
|
|
4815
|
+
).bind(userId, row.ats, row.slug).first();
|
|
4816
|
+
applyVerified = proven !== null;
|
|
4817
|
+
}
|
|
4818
|
+
let applicationStatus = null;
|
|
4819
|
+
if (userId) {
|
|
4820
|
+
const [stateRes, appRes] = await db.batch([
|
|
4821
|
+
db.prepare(
|
|
4822
|
+
`SELECT state, updated_at, ${TRACKING_COLS} FROM job_states WHERE job_id = ? AND user_id = ?`
|
|
4823
|
+
).bind(id, userId),
|
|
4824
|
+
db.prepare("SELECT status FROM applications WHERE job_id = ? AND user_id = ?").bind(id, userId)
|
|
4825
|
+
]);
|
|
4826
|
+
const stateRow = stateRes.results[0];
|
|
4827
|
+
state = asJobStateRead(stateRow?.state);
|
|
4828
|
+
stateUpdatedAt = stateRow?.updated_at ?? null;
|
|
4829
|
+
tracking = stateRow ? toTracking(stateRow) : null;
|
|
4830
|
+
const appRow = appRes.results[0];
|
|
4831
|
+
applicationStatus = appRow?.status ?? null;
|
|
4832
|
+
}
|
|
4833
|
+
const job = {
|
|
4834
|
+
id: row.id,
|
|
4835
|
+
title: row.title,
|
|
4836
|
+
company: row.company,
|
|
4837
|
+
location: row.location,
|
|
4838
|
+
workplace_type: row.workplace_type,
|
|
4839
|
+
job_type: row.job_type,
|
|
4840
|
+
salary_min: row.salary_min,
|
|
4841
|
+
salary_max: row.salary_max,
|
|
4842
|
+
source_site: row.source_site,
|
|
4843
|
+
url: row.url,
|
|
4844
|
+
posted_date: row.posted_date,
|
|
4845
|
+
scraped_at: row.scraped_at,
|
|
4846
|
+
ats: row.ats ?? null,
|
|
4847
|
+
slug: row.slug ?? null,
|
|
4848
|
+
slug_source: asSlugSource(row.slug_source ?? null),
|
|
4849
|
+
apply_tier: applyTier(
|
|
4850
|
+
row.application_url ?? row.url,
|
|
4851
|
+
row.ats ?? null
|
|
4852
|
+
),
|
|
4853
|
+
apply_verified: applyVerified,
|
|
4854
|
+
role_track: asRoleTrack(row.role_track ?? "unknown"),
|
|
4855
|
+
role_level: roleLevel,
|
|
4856
|
+
description: row.description,
|
|
4857
|
+
application_url: row.application_url,
|
|
4858
|
+
department: row.department,
|
|
4859
|
+
scraper_used: row.scraper_used,
|
|
4860
|
+
run_id: row.run_id,
|
|
4861
|
+
score,
|
|
4862
|
+
score_breakdown,
|
|
4863
|
+
state,
|
|
4864
|
+
application_status: asApplicationStatus(applicationStatus),
|
|
4865
|
+
state_updated_at: stateUpdatedAt,
|
|
4866
|
+
tracking
|
|
4867
|
+
};
|
|
4868
|
+
return c.json({ success: true, data: { job } }, 200);
|
|
4869
|
+
}
|
|
4870
|
+
);
|
|
4871
|
+
}
|
|
4872
|
+
function registerStateRoutes(app2) {
|
|
4873
|
+
app2.put("/jobs/:id/state", gateAuthed);
|
|
4874
|
+
app2.delete("/jobs/:id/state", gateAuthed);
|
|
4875
|
+
app2.openapi(
|
|
4876
|
+
createRoute({
|
|
4877
|
+
method: "put",
|
|
4878
|
+
path: "/jobs/{id}/state",
|
|
4879
|
+
tags: ["Jobs"],
|
|
4880
|
+
summary: "Set triage state for a job (per-user, upsert)",
|
|
4881
|
+
request: {
|
|
4882
|
+
params: z.object({ id: z.string() }),
|
|
4883
|
+
body: { content: { "application/json": { schema: SetJobStateSchema } } }
|
|
4884
|
+
},
|
|
4885
|
+
responses: {
|
|
4886
|
+
200: {
|
|
4887
|
+
description: "State updated",
|
|
4888
|
+
content: { "application/json": { schema: JobStateResponseSchema } }
|
|
4889
|
+
},
|
|
4890
|
+
403: {
|
|
4891
|
+
description: "Forbidden",
|
|
4892
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4893
|
+
},
|
|
4894
|
+
404: {
|
|
4895
|
+
description: "Job not found",
|
|
4896
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
}),
|
|
4900
|
+
async (c) => {
|
|
4901
|
+
const userId = await maybeUserId(c);
|
|
4902
|
+
if (!userId) {
|
|
4903
|
+
return c.json(
|
|
4904
|
+
{ success: false, error: "Forbidden", message: "Authentication required" },
|
|
4905
|
+
403
|
|
4906
|
+
);
|
|
4907
|
+
}
|
|
4908
|
+
const { id } = c.req.valid("param");
|
|
4909
|
+
const body = c.req.valid("json");
|
|
4910
|
+
const db = c.env.JOB_PLATFORM_DB;
|
|
4911
|
+
const exists = await db.prepare(
|
|
4912
|
+
`SELECT j.id, js.state AS prev FROM jobs j
|
|
4913
|
+
LEFT JOIN job_states js ON js.job_id = j.id AND js.user_id = ?
|
|
4914
|
+
WHERE j.id = ?`
|
|
4915
|
+
).bind(userId, id).first();
|
|
4916
|
+
if (!exists) {
|
|
4917
|
+
return c.json(
|
|
4918
|
+
{ success: false, error: "Not found", message: `Job '${id}' not found` },
|
|
4919
|
+
404
|
|
4920
|
+
);
|
|
4921
|
+
}
|
|
4922
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4923
|
+
const variantSlug = body.variant_slug ?? null;
|
|
4924
|
+
const upsert = db.prepare(
|
|
4925
|
+
`INSERT INTO job_states (job_id, user_id, state, notes, updated_at, variant_slug)
|
|
4926
|
+
VALUES (?, ?, ?, NULL, ?, ?)
|
|
4927
|
+
ON CONFLICT (job_id, user_id) DO UPDATE SET
|
|
4928
|
+
state = excluded.state,
|
|
4929
|
+
updated_at = excluded.updated_at,
|
|
4930
|
+
variant_slug = COALESCE(excluded.variant_slug, job_states.variant_slug)`
|
|
4931
|
+
).bind(id, userId, body.state, now, variantSlug);
|
|
4932
|
+
if (exists.prev === body.state) {
|
|
4933
|
+
await upsert.run();
|
|
4934
|
+
} else {
|
|
4935
|
+
await db.batch([
|
|
4936
|
+
upsert,
|
|
4937
|
+
db.prepare(
|
|
4938
|
+
`INSERT INTO job_events
|
|
4939
|
+
(job_id, user_id, kind, from_state, to_state, note, occurred_at, created_at)
|
|
4940
|
+
VALUES (?, ?, 'state', ?, ?, NULL, ?, ?)`
|
|
4941
|
+
).bind(id, userId, exists.prev, body.state, now, now)
|
|
4942
|
+
]);
|
|
4943
|
+
}
|
|
4944
|
+
return c.json(
|
|
4945
|
+
{
|
|
4946
|
+
success: true,
|
|
4947
|
+
data: { job_id: id, state: body.state, updated_at: now }
|
|
4741
4948
|
},
|
|
4742
4949
|
200
|
|
4743
4950
|
);
|
|
@@ -4746,18 +4953,74 @@ function registerFeedbackRoutes(app2) {
|
|
|
4746
4953
|
app2.openapi(
|
|
4747
4954
|
createRoute({
|
|
4748
4955
|
method: "delete",
|
|
4956
|
+
path: "/jobs/{id}/state",
|
|
4957
|
+
tags: ["Jobs"],
|
|
4958
|
+
summary: 'Clear triage state for a job — returns the row to implicit "new" and drops its timeline',
|
|
4959
|
+
request: {
|
|
4960
|
+
params: z.object({ id: z.string() })
|
|
4961
|
+
},
|
|
4962
|
+
responses: {
|
|
4963
|
+
200: {
|
|
4964
|
+
description: "State cleared (or already absent)",
|
|
4965
|
+
content: {
|
|
4966
|
+
"application/json": {
|
|
4967
|
+
schema: z.object({
|
|
4968
|
+
success: z.literal(true),
|
|
4969
|
+
data: z.object({ job_id: z.string(), deleted: z.boolean() })
|
|
4970
|
+
}).openapi("ClearJobStateResponse")
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
},
|
|
4974
|
+
403: {
|
|
4975
|
+
description: "Forbidden",
|
|
4976
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
}),
|
|
4980
|
+
async (c) => {
|
|
4981
|
+
const userId = await maybeUserId(c);
|
|
4982
|
+
if (!userId) {
|
|
4983
|
+
return c.json(
|
|
4984
|
+
{ success: false, error: "Forbidden", message: "Authentication required" },
|
|
4985
|
+
403
|
|
4986
|
+
);
|
|
4987
|
+
}
|
|
4988
|
+
const { id } = c.req.valid("param");
|
|
4989
|
+
const db = c.env.JOB_PLATFORM_DB;
|
|
4990
|
+
const [result] = await db.batch([
|
|
4991
|
+
db.prepare("DELETE FROM job_states WHERE job_id = ? AND user_id = ?").bind(id, userId),
|
|
4992
|
+
db.prepare("DELETE FROM job_events WHERE job_id = ? AND user_id = ?").bind(id, userId)
|
|
4993
|
+
]);
|
|
4994
|
+
const deleted = (result.meta?.changes ?? 0) > 0;
|
|
4995
|
+
return c.json({ success: true, data: { job_id: id, deleted } }, 200);
|
|
4996
|
+
}
|
|
4997
|
+
);
|
|
4998
|
+
}
|
|
4999
|
+
function registerFeedbackRoutes(app2) {
|
|
5000
|
+
app2.put("/jobs/:id/feedback", gateAuthed);
|
|
5001
|
+
app2.delete("/jobs/:id/feedback", gateAuthed);
|
|
5002
|
+
app2.openapi(
|
|
5003
|
+
createRoute({
|
|
5004
|
+
method: "put",
|
|
4749
5005
|
path: "/jobs/{id}/feedback",
|
|
4750
5006
|
tags: ["Jobs"],
|
|
4751
|
-
summary: "
|
|
4752
|
-
request: {
|
|
5007
|
+
summary: "Up/downvote a posting with an axis-aligned reason (per-user, upsert)",
|
|
5008
|
+
request: {
|
|
5009
|
+
params: z.object({ id: z.string() }),
|
|
5010
|
+
body: { content: { "application/json": { schema: SetJobFeedbackSchema } } }
|
|
5011
|
+
},
|
|
4753
5012
|
responses: {
|
|
4754
5013
|
200: {
|
|
4755
|
-
description: "Feedback
|
|
5014
|
+
description: "Feedback recorded",
|
|
4756
5015
|
content: { "application/json": { schema: JobFeedbackResponseSchema } }
|
|
4757
5016
|
},
|
|
4758
5017
|
403: {
|
|
4759
5018
|
description: "Forbidden",
|
|
4760
5019
|
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
5020
|
+
},
|
|
5021
|
+
404: {
|
|
5022
|
+
description: "Job not found",
|
|
5023
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
4761
5024
|
}
|
|
4762
5025
|
}
|
|
4763
5026
|
}),
|
|
@@ -4770,22 +5033,78 @@ function registerFeedbackRoutes(app2) {
|
|
|
4770
5033
|
);
|
|
4771
5034
|
}
|
|
4772
5035
|
const { id } = c.req.valid("param");
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
).bind(
|
|
5036
|
+
const body = c.req.valid("json");
|
|
5037
|
+
const db = c.env.JOB_PLATFORM_DB;
|
|
5038
|
+
const exists = await db.prepare("SELECT id FROM jobs WHERE id = ?").bind(id).first();
|
|
5039
|
+
if (!exists) {
|
|
5040
|
+
return c.json(
|
|
5041
|
+
{ success: false, error: "Not found", message: `Job '${id}' not found` },
|
|
5042
|
+
404
|
|
5043
|
+
);
|
|
5044
|
+
}
|
|
5045
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5046
|
+
const reasonCsv = body.reasons.length ? body.reasons.join(",") : null;
|
|
5047
|
+
await db.prepare(
|
|
5048
|
+
`INSERT INTO job_feedback (id, user_id, job_id, vote, reason, created_at, processed_at)
|
|
5049
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)
|
|
5050
|
+
ON CONFLICT (user_id, job_id) DO UPDATE SET
|
|
5051
|
+
vote = excluded.vote,
|
|
5052
|
+
reason = excluded.reason,
|
|
5053
|
+
created_at = excluded.created_at,
|
|
5054
|
+
processed_at = NULL`
|
|
5055
|
+
).bind(crypto.randomUUID(), userId, id, body.vote, reasonCsv, now).run();
|
|
4776
5056
|
return c.json(
|
|
4777
5057
|
{
|
|
4778
5058
|
success: true,
|
|
4779
|
-
data: { job_id: id, vote:
|
|
5059
|
+
data: { job_id: id, vote: body.vote, reasons: body.reasons, updated_at: now }
|
|
5060
|
+
},
|
|
5061
|
+
200
|
|
5062
|
+
);
|
|
5063
|
+
}
|
|
5064
|
+
);
|
|
5065
|
+
app2.openapi(
|
|
5066
|
+
createRoute({
|
|
5067
|
+
method: "delete",
|
|
5068
|
+
path: "/jobs/{id}/feedback",
|
|
5069
|
+
tags: ["Jobs"],
|
|
5070
|
+
summary: "Clear the vote on a posting — idempotent",
|
|
5071
|
+
request: { params: z.object({ id: z.string() }) },
|
|
5072
|
+
responses: {
|
|
5073
|
+
200: {
|
|
5074
|
+
description: "Feedback cleared",
|
|
5075
|
+
content: { "application/json": { schema: JobFeedbackResponseSchema } }
|
|
5076
|
+
},
|
|
5077
|
+
403: {
|
|
5078
|
+
description: "Forbidden",
|
|
5079
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
}),
|
|
5083
|
+
async (c) => {
|
|
5084
|
+
const userId = await maybeUserId(c);
|
|
5085
|
+
if (!userId) {
|
|
5086
|
+
return c.json(
|
|
5087
|
+
{ success: false, error: "Forbidden", message: "Authentication required" },
|
|
5088
|
+
403
|
|
5089
|
+
);
|
|
5090
|
+
}
|
|
5091
|
+
const { id } = c.req.valid("param");
|
|
5092
|
+
await c.env.JOB_PLATFORM_DB.prepare(
|
|
5093
|
+
"DELETE FROM job_feedback WHERE user_id = ? AND job_id = ?"
|
|
5094
|
+
).bind(userId, id).run();
|
|
5095
|
+
return c.json(
|
|
5096
|
+
{
|
|
5097
|
+
success: true,
|
|
5098
|
+
data: { job_id: id, vote: null, reasons: [], updated_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
4780
5099
|
},
|
|
4781
5100
|
200
|
|
4782
5101
|
);
|
|
4783
5102
|
}
|
|
4784
5103
|
);
|
|
4785
5104
|
}
|
|
4786
|
-
async function loadTailoringFields(db, id) {
|
|
4787
|
-
return db.prepare("SELECT title, company, description, location, workplace_type FROM jobs WHERE id = ?").bind(id).first();
|
|
4788
|
-
}
|
|
5105
|
+
async function loadTailoringFields(db, id) {
|
|
5106
|
+
return db.prepare("SELECT title, company, description, location, workplace_type FROM jobs WHERE id = ?").bind(id).first();
|
|
5107
|
+
}
|
|
4789
5108
|
function resumeViaEdge(env, path, init) {
|
|
4790
5109
|
if (!env.EDGE) throw new Error("EDGE service binding not configured");
|
|
4791
5110
|
const key = env.HADOKU_SERVICE_KEY?.trim();
|
|
@@ -5797,7 +6116,7 @@ async function delistedReason(db, jobId) {
|
|
|
5797
6116
|
if (newest - seen <= DELISTED_LAG_MS) return null;
|
|
5798
6117
|
return `this posting has not appeared in a scrape of the ${job.ats} board '${job.slug}' since ${job.last_seen_at}, while other jobs on that board were seen as recently as ${board.newest} — it looks taken down`;
|
|
5799
6118
|
}
|
|
5800
|
-
const FORBIDDEN
|
|
6119
|
+
const FORBIDDEN = {
|
|
5801
6120
|
success: false,
|
|
5802
6121
|
error: "Forbidden",
|
|
5803
6122
|
message: "Authentication required"
|
|
@@ -6096,7 +6415,7 @@ function registerApplicationRoutes(app2) {
|
|
|
6096
6415
|
}),
|
|
6097
6416
|
async (c) => {
|
|
6098
6417
|
const userId = await maybeUserId(c);
|
|
6099
|
-
if (!userId) return c.json(FORBIDDEN
|
|
6418
|
+
if (!userId) return c.json(FORBIDDEN, 403);
|
|
6100
6419
|
const { id } = c.req.valid("param");
|
|
6101
6420
|
const db = c.env.JOB_PLATFORM_DB;
|
|
6102
6421
|
const existing = await db.prepare("SELECT * FROM applications WHERE id = ? AND user_id = ?").bind(id, userId).first();
|
|
@@ -6340,318 +6659,6 @@ function registerFillRoutes(app2) {
|
|
|
6340
6659
|
);
|
|
6341
6660
|
});
|
|
6342
6661
|
}
|
|
6343
|
-
const IDENTITY_ERRORS = {
|
|
6344
|
-
404: {
|
|
6345
|
-
description: "No such owner name, or no such job",
|
|
6346
|
-
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
6347
|
-
},
|
|
6348
|
-
409: {
|
|
6349
|
-
description: "That owner name has never signed in, or the posting is not in the pipeline",
|
|
6350
|
-
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
6351
|
-
},
|
|
6352
|
-
503: {
|
|
6353
|
-
description: "Identity could not be resolved right now — retry",
|
|
6354
|
-
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
6355
|
-
}
|
|
6356
|
-
};
|
|
6357
|
-
const FORBIDDEN = {
|
|
6358
|
-
description: "Forbidden",
|
|
6359
|
-
content: { "application/json": { schema: IdentityErrorResponseSchema } }
|
|
6360
|
-
};
|
|
6361
|
-
const EVENT_COLS = "id, job_id, kind, from_state, to_state, note, occurred_at, created_at";
|
|
6362
|
-
const TRACKING_COLS = "notes, priority, follow_up_at, contact_name, contact_email";
|
|
6363
|
-
function stateOrNull(v) {
|
|
6364
|
-
const s = asJobStateRead(v);
|
|
6365
|
-
return s === "new" ? null : s;
|
|
6366
|
-
}
|
|
6367
|
-
function toEvent(r) {
|
|
6368
|
-
return {
|
|
6369
|
-
...r,
|
|
6370
|
-
kind: r.kind,
|
|
6371
|
-
from_state: stateOrNull(r.from_state),
|
|
6372
|
-
to_state: stateOrNull(r.to_state)
|
|
6373
|
-
};
|
|
6374
|
-
}
|
|
6375
|
-
function asPriority(p) {
|
|
6376
|
-
return p === 1 || p === 2 || p === 3 ? p : null;
|
|
6377
|
-
}
|
|
6378
|
-
function toTracking(r) {
|
|
6379
|
-
return {
|
|
6380
|
-
notes: r.notes,
|
|
6381
|
-
priority: asPriority(r.priority),
|
|
6382
|
-
follow_up_at: r.follow_up_at,
|
|
6383
|
-
contact_name: r.contact_name,
|
|
6384
|
-
contact_email: r.contact_email
|
|
6385
|
-
};
|
|
6386
|
-
}
|
|
6387
|
-
function notInPipeline(id) {
|
|
6388
|
-
return {
|
|
6389
|
-
success: false,
|
|
6390
|
-
error: "Conflict",
|
|
6391
|
-
message: `Job '${id}' is not in your pipeline — set its state first (PUT /jobs/${id}/state)`
|
|
6392
|
-
};
|
|
6393
|
-
}
|
|
6394
|
-
function jobNotFound(id) {
|
|
6395
|
-
return { success: false, error: "Not found", message: `Job '${id}' not found` };
|
|
6396
|
-
}
|
|
6397
|
-
async function stateOf(db, jobId, userId) {
|
|
6398
|
-
const row = await db.prepare(
|
|
6399
|
-
`SELECT j.id, js.state FROM jobs j
|
|
6400
|
-
LEFT JOIN job_states js ON js.job_id = j.id AND js.user_id = ?
|
|
6401
|
-
WHERE j.id = ?`
|
|
6402
|
-
).bind(userId, jobId).first();
|
|
6403
|
-
if (!row) return "missing";
|
|
6404
|
-
return row.state ? "tracked" : "untracked";
|
|
6405
|
-
}
|
|
6406
|
-
function registerPipelineRoutes(app2) {
|
|
6407
|
-
app2.get("/pipeline", gateAuthed);
|
|
6408
|
-
app2.patch("/jobs/:id/tracking", gateAuthed);
|
|
6409
|
-
app2.get("/jobs/:id/events", gateAuthed);
|
|
6410
|
-
app2.post("/jobs/:id/events", gateAuthed);
|
|
6411
|
-
app2.delete("/jobs/:id/events/:eventId", gateAuthed);
|
|
6412
|
-
app2.openapi(
|
|
6413
|
-
createRoute({
|
|
6414
|
-
method: "get",
|
|
6415
|
-
path: "/pipeline",
|
|
6416
|
-
tags: ["Pipeline"],
|
|
6417
|
-
summary: "Every posting in the caller's pipeline, most recently changed first",
|
|
6418
|
-
request: {
|
|
6419
|
-
query: ownerNameQuery.extend({
|
|
6420
|
-
include_dismissed: z.enum(["true", "false"]).optional().openapi({
|
|
6421
|
-
description: "Dismissed postings are left out unless this is true."
|
|
6422
|
-
})
|
|
6423
|
-
})
|
|
6424
|
-
},
|
|
6425
|
-
responses: {
|
|
6426
|
-
200: {
|
|
6427
|
-
description: "Pipeline",
|
|
6428
|
-
content: { "application/json": { schema: PipelineResponseSchema } }
|
|
6429
|
-
},
|
|
6430
|
-
403: FORBIDDEN,
|
|
6431
|
-
...IDENTITY_ERRORS
|
|
6432
|
-
}
|
|
6433
|
-
}),
|
|
6434
|
-
async (c) => {
|
|
6435
|
-
const { ownerName, include_dismissed } = c.req.valid("query");
|
|
6436
|
-
const who = await effectiveUserId(c, ownerName);
|
|
6437
|
-
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
6438
|
-
const dismissed = include_dismissed === "true" ? "" : "AND js.state != 'dismissed'";
|
|
6439
|
-
const rows = await c.env.JOB_PLATFORM_DB.prepare(
|
|
6440
|
-
`SELECT js.job_id, j.title, j.company, j.location, j.workplace_type,
|
|
6441
|
-
j.salary_min, j.salary_max, j.url, j.posted_date,
|
|
6442
|
-
js.state, js.updated_at, js.variant_slug,
|
|
6443
|
-
js.notes, js.priority, js.follow_up_at, js.contact_name, js.contact_email,
|
|
6444
|
-
ap.status AS application_status,
|
|
6445
|
-
(SELECT MIN(e.occurred_at) FROM job_events e
|
|
6446
|
-
WHERE e.job_id = js.job_id AND e.user_id = js.user_id
|
|
6447
|
-
AND e.kind = 'state' AND e.to_state = 'applied') AS applied_at,
|
|
6448
|
-
(SELECT MAX(e.occurred_at) FROM job_events e
|
|
6449
|
-
WHERE e.job_id = js.job_id AND e.user_id = js.user_id) AS last_event_at
|
|
6450
|
-
FROM job_states js
|
|
6451
|
-
JOIN jobs j ON j.id = js.job_id
|
|
6452
|
-
LEFT JOIN applications ap ON ap.job_id = js.job_id AND ap.user_id = js.user_id
|
|
6453
|
-
WHERE js.user_id = ? ${dismissed}
|
|
6454
|
-
ORDER BY js.updated_at DESC`
|
|
6455
|
-
).bind(who.userId).all();
|
|
6456
|
-
const items = rows.results.flatMap((r) => {
|
|
6457
|
-
const state = stateOrNull(r.state);
|
|
6458
|
-
if (!state) return [];
|
|
6459
|
-
return [
|
|
6460
|
-
{
|
|
6461
|
-
job_id: r.job_id,
|
|
6462
|
-
title: r.title,
|
|
6463
|
-
company: r.company,
|
|
6464
|
-
location: r.location,
|
|
6465
|
-
workplace_type: r.workplace_type,
|
|
6466
|
-
salary_min: r.salary_min,
|
|
6467
|
-
salary_max: r.salary_max,
|
|
6468
|
-
url: r.url,
|
|
6469
|
-
posted_date: r.posted_date,
|
|
6470
|
-
state,
|
|
6471
|
-
updated_at: r.updated_at,
|
|
6472
|
-
tracking: toTracking(r),
|
|
6473
|
-
variant_slug: r.variant_slug,
|
|
6474
|
-
application_status: asApplicationStatus(r.application_status),
|
|
6475
|
-
applied_at: r.applied_at,
|
|
6476
|
-
last_event_at: r.last_event_at
|
|
6477
|
-
}
|
|
6478
|
-
];
|
|
6479
|
-
});
|
|
6480
|
-
return c.json({ success: true, data: { items } }, 200);
|
|
6481
|
-
}
|
|
6482
|
-
);
|
|
6483
|
-
app2.openapi(
|
|
6484
|
-
createRoute({
|
|
6485
|
-
method: "patch",
|
|
6486
|
-
path: "/jobs/{id}/tracking",
|
|
6487
|
-
tags: ["Pipeline"],
|
|
6488
|
-
summary: "Change what the caller tracks about one application; null clears a field",
|
|
6489
|
-
request: {
|
|
6490
|
-
params: z.object({ id: z.string() }),
|
|
6491
|
-
query: ownerNameQuery,
|
|
6492
|
-
body: { content: { "application/json": { schema: SetTrackingSchema } } }
|
|
6493
|
-
},
|
|
6494
|
-
responses: {
|
|
6495
|
-
200: {
|
|
6496
|
-
description: "Tracking after the change",
|
|
6497
|
-
content: { "application/json": { schema: TrackingResponseSchema } }
|
|
6498
|
-
},
|
|
6499
|
-
403: FORBIDDEN,
|
|
6500
|
-
...IDENTITY_ERRORS
|
|
6501
|
-
}
|
|
6502
|
-
}),
|
|
6503
|
-
async (c) => {
|
|
6504
|
-
const { id } = c.req.valid("param");
|
|
6505
|
-
const { ownerName } = c.req.valid("query");
|
|
6506
|
-
const body = c.req.valid("json");
|
|
6507
|
-
const who = await effectiveUserId(c, ownerName);
|
|
6508
|
-
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
6509
|
-
const db = c.env.JOB_PLATFORM_DB;
|
|
6510
|
-
const sets = [];
|
|
6511
|
-
const binds = [];
|
|
6512
|
-
for (const col of [
|
|
6513
|
-
"notes",
|
|
6514
|
-
"priority",
|
|
6515
|
-
"follow_up_at",
|
|
6516
|
-
"contact_name",
|
|
6517
|
-
"contact_email"
|
|
6518
|
-
]) {
|
|
6519
|
-
const v = body[col];
|
|
6520
|
-
if (v === void 0) continue;
|
|
6521
|
-
sets.push(`${col} = ?`);
|
|
6522
|
-
binds.push(typeof v === "string" && v.trim() === "" ? null : v);
|
|
6523
|
-
}
|
|
6524
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6525
|
-
const row = await db.prepare(
|
|
6526
|
-
`UPDATE job_states SET ${sets.join(", ")}, updated_at = ?
|
|
6527
|
-
WHERE job_id = ? AND user_id = ?
|
|
6528
|
-
RETURNING ${TRACKING_COLS}`
|
|
6529
|
-
).bind(...binds, now, id, who.userId).first();
|
|
6530
|
-
if (!row) {
|
|
6531
|
-
const s = await stateOf(db, id, who.userId);
|
|
6532
|
-
return s === "missing" ? c.json(jobNotFound(id), 404) : c.json(notInPipeline(id), 409);
|
|
6533
|
-
}
|
|
6534
|
-
return c.json(
|
|
6535
|
-
{ success: true, data: { job_id: id, tracking: toTracking(row) } },
|
|
6536
|
-
200
|
|
6537
|
-
);
|
|
6538
|
-
}
|
|
6539
|
-
);
|
|
6540
|
-
app2.openapi(
|
|
6541
|
-
createRoute({
|
|
6542
|
-
method: "get",
|
|
6543
|
-
path: "/jobs/{id}/events",
|
|
6544
|
-
tags: ["Pipeline"],
|
|
6545
|
-
summary: "One posting's timeline for the caller, oldest first",
|
|
6546
|
-
request: { params: z.object({ id: z.string() }), query: ownerNameQuery },
|
|
6547
|
-
responses: {
|
|
6548
|
-
200: {
|
|
6549
|
-
description: "Timeline (empty for a posting with none)",
|
|
6550
|
-
content: { "application/json": { schema: JobEventsResponseSchema } }
|
|
6551
|
-
},
|
|
6552
|
-
403: FORBIDDEN,
|
|
6553
|
-
...IDENTITY_ERRORS
|
|
6554
|
-
}
|
|
6555
|
-
}),
|
|
6556
|
-
async (c) => {
|
|
6557
|
-
const { id } = c.req.valid("param");
|
|
6558
|
-
const { ownerName } = c.req.valid("query");
|
|
6559
|
-
const who = await effectiveUserId(c, ownerName);
|
|
6560
|
-
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
6561
|
-
const rows = await c.env.JOB_PLATFORM_DB.prepare(
|
|
6562
|
-
`SELECT ${EVENT_COLS} FROM job_events
|
|
6563
|
-
WHERE job_id = ? AND user_id = ?
|
|
6564
|
-
ORDER BY occurred_at, id`
|
|
6565
|
-
).bind(id, who.userId).all();
|
|
6566
|
-
return c.json({ success: true, data: { events: rows.results.map(toEvent) } }, 200);
|
|
6567
|
-
}
|
|
6568
|
-
);
|
|
6569
|
-
app2.openapi(
|
|
6570
|
-
createRoute({
|
|
6571
|
-
method: "post",
|
|
6572
|
-
path: "/jobs/{id}/events",
|
|
6573
|
-
tags: ["Pipeline"],
|
|
6574
|
-
summary: "Log something that happened on one application",
|
|
6575
|
-
request: {
|
|
6576
|
-
params: z.object({ id: z.string() }),
|
|
6577
|
-
query: ownerNameQuery,
|
|
6578
|
-
body: { content: { "application/json": { schema: AddJobEventSchema } } }
|
|
6579
|
-
},
|
|
6580
|
-
responses: {
|
|
6581
|
-
201: {
|
|
6582
|
-
description: "Logged",
|
|
6583
|
-
content: { "application/json": { schema: JobEventResponseSchema } }
|
|
6584
|
-
},
|
|
6585
|
-
403: FORBIDDEN,
|
|
6586
|
-
...IDENTITY_ERRORS
|
|
6587
|
-
}
|
|
6588
|
-
}),
|
|
6589
|
-
async (c) => {
|
|
6590
|
-
const { id } = c.req.valid("param");
|
|
6591
|
-
const { ownerName } = c.req.valid("query");
|
|
6592
|
-
const body = c.req.valid("json");
|
|
6593
|
-
const who = await effectiveUserId(c, ownerName);
|
|
6594
|
-
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
6595
|
-
const db = c.env.JOB_PLATFORM_DB;
|
|
6596
|
-
const s = await stateOf(db, id, who.userId);
|
|
6597
|
-
if (s === "missing") return c.json(jobNotFound(id), 404);
|
|
6598
|
-
if (s === "untracked") return c.json(notInPipeline(id), 409);
|
|
6599
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6600
|
-
const occurred = body.occurred_at ? new Date(
|
|
6601
|
-
body.occurred_at.length === 10 ? `${body.occurred_at}T00:00:00Z` : body.occurred_at
|
|
6602
|
-
).toISOString() : now;
|
|
6603
|
-
const row = await db.prepare(
|
|
6604
|
-
`INSERT INTO job_events
|
|
6605
|
-
(job_id, user_id, kind, from_state, to_state, note, occurred_at, created_at)
|
|
6606
|
-
VALUES (?, ?, ?, NULL, NULL, ?, ?, ?)
|
|
6607
|
-
RETURNING ${EVENT_COLS}`
|
|
6608
|
-
).bind(id, who.userId, body.kind, body.note, occurred, now).first();
|
|
6609
|
-
if (!row) throw new Error("job_events INSERT … RETURNING returned no row");
|
|
6610
|
-
return c.json({ success: true, data: { event: toEvent(row) } }, 201);
|
|
6611
|
-
}
|
|
6612
|
-
);
|
|
6613
|
-
app2.openapi(
|
|
6614
|
-
createRoute({
|
|
6615
|
-
method: "delete",
|
|
6616
|
-
path: "/jobs/{id}/events/{eventId}",
|
|
6617
|
-
tags: ["Pipeline"],
|
|
6618
|
-
summary: "Remove a note/interview/contact/follow-up the caller logged",
|
|
6619
|
-
request: {
|
|
6620
|
-
params: z.object({ id: z.string(), eventId: z.coerce.number().int().positive() }),
|
|
6621
|
-
query: ownerNameQuery
|
|
6622
|
-
},
|
|
6623
|
-
responses: {
|
|
6624
|
-
200: {
|
|
6625
|
-
description: "Removed (or already absent)",
|
|
6626
|
-
content: {
|
|
6627
|
-
"application/json": {
|
|
6628
|
-
schema: z.object({
|
|
6629
|
-
success: z.literal(true),
|
|
6630
|
-
data: z.object({ id: z.number().int(), deleted: z.boolean() })
|
|
6631
|
-
}).openapi("DeleteJobEventResponse")
|
|
6632
|
-
}
|
|
6633
|
-
}
|
|
6634
|
-
},
|
|
6635
|
-
403: FORBIDDEN,
|
|
6636
|
-
...IDENTITY_ERRORS
|
|
6637
|
-
}
|
|
6638
|
-
}),
|
|
6639
|
-
async (c) => {
|
|
6640
|
-
const { id, eventId } = c.req.valid("param");
|
|
6641
|
-
const { ownerName } = c.req.valid("query");
|
|
6642
|
-
const who = await effectiveUserId(c, ownerName);
|
|
6643
|
-
if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
|
|
6644
|
-
const res = await c.env.JOB_PLATFORM_DB.prepare(
|
|
6645
|
-
`DELETE FROM job_events
|
|
6646
|
-
WHERE id = ? AND job_id = ? AND user_id = ? AND kind != 'state'`
|
|
6647
|
-
).bind(eventId, id, who.userId).run();
|
|
6648
|
-
return c.json(
|
|
6649
|
-
{ success: true, data: { id: eventId, deleted: (res.meta?.changes ?? 0) > 0 } },
|
|
6650
|
-
200
|
|
6651
|
-
);
|
|
6652
|
-
}
|
|
6653
|
-
);
|
|
6654
|
-
}
|
|
6655
6662
|
const ERRORS$1 = {
|
|
6656
6663
|
403: {
|
|
6657
6664
|
description: "Forbidden",
|