From bb0c511ed6ead29a7f2712965b6d17e3fbe41449 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 06:06:24 +0000 Subject: [PATCH 1/2] Fix codemode job metadata mutations Co-authored-by: Kent C. Dodds --- packages/worker/src/jobs/service.node.test.ts | 2 + packages/worker/src/jobs/service.ts | 37 +- .../mcp/run-codemode-registry.node.test.ts | 414 ++++++++++++++++++ 3 files changed, 440 insertions(+), 13 deletions(-) diff --git a/packages/worker/src/jobs/service.node.test.ts b/packages/worker/src/jobs/service.node.test.ts index 6234bccc91..9b18ba0f8e 100644 --- a/packages/worker/src/jobs/service.node.test.ts +++ b/packages/worker/src/jobs/service.node.test.ts @@ -1622,6 +1622,7 @@ test('getJobInspection returns persisted params after a job update', async () => }, }) + repoMockModule.syncArtifactSourceSnapshot.mockClear() const updated = await updateJob({ env, callerContext, @@ -1640,6 +1641,7 @@ test('getJobInspection returns persisted params after a job update', async () => expect(updated.params).toEqual({ bridgeId: 'ZPGI01117' }) expect(inspected.job.params).toEqual({ bridgeId: 'ZPGI01117' }) + expect(repoMockModule.syncArtifactSourceSnapshot).not.toHaveBeenCalled() }) test('executeJobOnce binds scheduled jobs to writable storage', async () => { diff --git a/packages/worker/src/jobs/service.ts b/packages/worker/src/jobs/service.ts index 2bafe7b45e..331cb417e9 100644 --- a/packages/worker/src/jobs/service.ts +++ b/packages/worker/src/jobs/service.ts @@ -922,6 +922,15 @@ function resolveUpdatedShape(input: { } } +function shouldSyncJobSourceForUpdate(body: JobUpdateInput) { + return ( + body.code !== undefined || + body.name !== undefined || + body.schedule !== undefined || + body.timezone !== undefined + ) +} + export async function createJob(input: { env: Env callerContext: McpCallerContext @@ -1131,19 +1140,21 @@ export async function updateJob(input: { }) : existing.nextRunAt, } - const syncedPublishedCommit = await syncArtifactSourceSnapshot({ - env: input.env, - userId: callerContext.user.userId, - baseUrl: callerContext.baseUrl, - sourceId: updated.sourceId, - bootstrapAccess: null, - files: buildJobSourceFiles({ - job: toJobView(updated), - moduleSource: shape.moduleSource ?? null, - }), - }) - if (syncedPublishedCommit) { - updated.publishedCommit = syncedPublishedCommit + if (shouldSyncJobSourceForUpdate(input.body)) { + const syncedPublishedCommit = await syncArtifactSourceSnapshot({ + env: input.env, + userId: callerContext.user.userId, + baseUrl: callerContext.baseUrl, + sourceId: updated.sourceId, + bootstrapAccess: null, + files: buildJobSourceFiles({ + job: toJobView(updated), + moduleSource: shape.moduleSource ?? null, + }), + }) + if (syncedPublishedCommit) { + updated.publishedCommit = syncedPublishedCommit + } } const nextCallerContextJson = serializeCallerContext(callerContext) const didUpdate = await updateJobRow({ diff --git a/packages/worker/src/mcp/run-codemode-registry.node.test.ts b/packages/worker/src/mcp/run-codemode-registry.node.test.ts index 4e712ba85d..507bd6c017 100644 --- a/packages/worker/src/mcp/run-codemode-registry.node.test.ts +++ b/packages/worker/src/mcp/run-codemode-registry.node.test.ts @@ -12,6 +12,11 @@ import { import { PackageSecretMountError } from '#mcp/secrets/package-access.ts' import * as packageAccess from '#mcp/secrets/package-access.ts' import * as secretService from '#mcp/secrets/service.ts' +import { + type JobRecord, + type PersistedJobCallerContext, +} from '#worker/jobs/types.ts' +import { type EntitySourceRow } from '#worker/repo/types.ts' test('createWorkflowTools creates package workflow instances from package context', async () => { const created: Array> = [] @@ -189,6 +194,415 @@ vi.mock('#worker/package-runtime/module-graph.ts', async () => { } }) +function createJobMutationDatabase(input: { + jobs: Array> + entitySources: Array> + repoSessions?: Array> + publishedBundleArtifacts?: Array> +}) { + const tables = new Map>>([ + ['jobs', structuredClone(input.jobs)], + ['entity_sources', structuredClone(input.entitySources)], + ['repo_sessions', structuredClone(input.repoSessions ?? [])], + [ + 'published_bundle_artifacts', + structuredClone(input.publishedBundleArtifacts ?? []), + ], + ]) + + const clone = (value: T): T => structuredClone(value) + const table = (name: string) => { + const rows = tables.get(name) + if (!rows) throw new Error(`Unknown test table: ${name}`) + return rows + } + const selectOne = ( + name: string, + predicate: (row: Record) => boolean, + ) => clone(table(name).find(predicate) ?? null) + const selectAll = ( + name: string, + predicate: (row: Record) => boolean, + ) => clone(table(name).filter(predicate)) + const deleteWhere = ( + name: string, + predicate: (row: Record) => boolean, + ) => { + const rows = table(name) + const remaining = rows.filter((row) => !predicate(row)) + tables.set(name, remaining) + return rows.length - remaining.length + } + + return { + prepare(query: string) { + const normalized = query.replace(/\s+/g, ' ').trim() + return { + bind(...params: Array) { + return { + async first>() { + if ( + normalized === 'SELECT * FROM jobs WHERE id = ? AND user_id = ?' + ) { + return selectOne( + 'jobs', + (row) => + row['id'] === params[0] && row['user_id'] === params[1], + ) as T | null + } + if (normalized === 'SELECT * FROM entity_sources WHERE id = ?') { + return selectOne( + 'entity_sources', + (row) => row['id'] === params[0], + ) as T | null + } + throw new Error(`Unsupported first query: ${query}`) + }, + async all>() { + if ( + normalized === + 'SELECT * FROM repo_sessions WHERE user_id = ? AND source_id = ? ORDER BY updated_at DESC' + ) { + return { + results: selectAll( + 'repo_sessions', + (row) => + row['user_id'] === params[0] && + row['source_id'] === params[1], + ) as T[], + } + } + if ( + normalized === + 'SELECT * FROM published_bundle_artifacts WHERE user_id = ? AND source_id = ? ORDER BY updated_at DESC, created_at DESC' + ) { + return { + results: selectAll( + 'published_bundle_artifacts', + (row) => + row['user_id'] === params[0] && + row['source_id'] === params[1], + ) as T[], + } + } + throw new Error(`Unsupported all query: ${query}`) + }, + async run() { + if (normalized.startsWith('UPDATE jobs SET')) { + const id = params[21] + const userId = params[22] + const existing = selectOne( + 'jobs', + (row) => row['id'] === id && row['user_id'] === userId, + ) + if (!existing) return { meta: { changes: 0, last_row_id: 0 } } + const updated = { + ...existing, + name: params[0], + source_id: params[1], + published_commit: params[2], + repo_check_policy_json: params[3], + storage_id: params[4], + params_json: params[5], + schedule_json: params[6], + timezone: params[7], + enabled: params[8], + kill_switch_enabled: params[9], + caller_context_json: params[10], + updated_at: params[11], + last_run_at: params[12], + last_run_status: params[13], + last_run_error: params[14], + last_duration_ms: params[15], + next_run_at: params[16], + run_count: params[17], + success_count: params[18], + error_count: params[19], + run_history_json: params[20], + } + const rows = table('jobs') + const index = rows.findIndex( + (row) => row['id'] === id && row['user_id'] === userId, + ) + rows[index] = updated + return { meta: { changes: 1, last_row_id: 0 } } + } + if ( + normalized === 'DELETE FROM jobs WHERE id = ? AND user_id = ?' + ) { + return { + meta: { + changes: deleteWhere( + 'jobs', + (row) => + row['id'] === params[0] && row['user_id'] === params[1], + ), + last_row_id: 0, + }, + } + } + if ( + normalized === + 'DELETE FROM published_bundle_artifacts WHERE user_id = ? AND source_id = ?' + ) { + return { + meta: { + changes: deleteWhere( + 'published_bundle_artifacts', + (row) => + row['user_id'] === params[0] && + row['source_id'] === params[1], + ), + last_row_id: 0, + }, + } + } + if ( + normalized === + 'DELETE FROM entity_sources WHERE id = ? AND user_id = ?' + ) { + return { + meta: { + changes: deleteWhere( + 'entity_sources', + (row) => + row['id'] === params[0] && row['user_id'] === params[1], + ), + last_row_id: 0, + }, + } + } + throw new Error(`Unsupported run query: ${query}`) + }, + } + }, + } + }, + } as D1Database +} + +function createJobMutationKv() { + const deletedKeys: Array = [] + return { + deletedKeys, + async get() { + return null + }, + async put() {}, + async delete(key: string) { + deletedKeys.push(key) + }, + } as unknown as KVNamespace & { deletedKeys: Array } +} + +function createJobRow( + job: JobRecord, + callerContext: PersistedJobCallerContext, +) { + return { + id: job.id, + user_id: job.userId, + name: job.name, + source_id: job.sourceId, + published_commit: job.publishedCommit, + repo_check_policy_json: job.repoCheckPolicy + ? JSON.stringify(job.repoCheckPolicy) + : null, + storage_id: job.storageId, + params_json: job.params ? JSON.stringify(job.params) : null, + schedule_json: JSON.stringify(job.schedule), + timezone: job.timezone, + enabled: job.enabled ? 1 : 0, + kill_switch_enabled: job.killSwitchEnabled ? 1 : 0, + caller_context_json: JSON.stringify(callerContext), + created_at: job.createdAt, + updated_at: job.updatedAt, + last_run_at: job.lastRunAt ?? null, + last_run_status: job.lastRunStatus ?? null, + last_run_error: job.lastRunError ?? null, + last_duration_ms: job.lastDurationMs ?? null, + next_run_at: job.nextRunAt, + run_count: job.runCount, + success_count: job.successCount, + error_count: job.errorCount, + run_history_json: JSON.stringify(job.runHistory), + } +} + +function createEntitySourceRow(input: { + userId: string + jobId: string + sourceId: string + repoId: string +}): EntitySourceRow { + return { + id: input.sourceId, + user_id: input.userId, + entity_kind: 'job', + entity_id: input.jobId, + repo_id: input.repoId, + published_commit: 'published-commit-1', + indexed_commit: null, + manifest_path: 'kody.json', + source_root: '/', + last_external_check_at: null, + created_at: '2026-04-16T00:00:00.000Z', + updated_at: '2026-04-16T00:00:00.000Z', + } +} + +test('buildCodemodeFns updates and deletes jobs through production-shaped bindings', async () => { + const callerContext = createMcpCallerContext({ + baseUrl: 'https://heykody.dev', + user: { + userId: 'user-123', + email: 'user@example.com', + displayName: 'User Example', + }, + storageContext: { + sessionId: null, + appId: 'app-123', + storageId: null, + }, + }) as PersistedJobCallerContext + const job: JobRecord = { + version: 1, + id: '504513c3-f29e-47f0-9ea1-402569ebef54', + userId: callerContext.user.userId, + name: 'hrv-discord-reaction-poller', + sourceId: 'job-source-1', + publishedCommit: 'published-commit-1', + storageId: `job:504513c3-f29e-47f0-9ea1-402569ebef54`, + params: { + channelId: 'discord-channel-1', + }, + schedule: { + type: 'interval', + every: '5m', + }, + timezone: 'UTC', + enabled: true, + killSwitchEnabled: false, + createdAt: '2026-04-16T00:00:00.000Z', + updatedAt: '2026-04-16T00:00:00.000Z', + nextRunAt: '2026-04-16T00:05:00.000Z', + runCount: 0, + successCount: 0, + errorCount: 0, + runHistory: [], + } + const db = createJobMutationDatabase({ + jobs: [createJobRow(job, callerContext)], + entitySources: [ + createEntitySourceRow({ + userId: callerContext.user.userId, + jobId: job.id, + sourceId: job.sourceId, + repoId: `job-${job.id}`, + }), + ], + }) + const kv = createJobMutationKv() + const repoSessionAccesses: Array = [] + const env = { + APP_DB: db, + SENTRY_ENVIRONMENT: 'production', + CLOUDFLARE_ACCOUNT_ID: 'acct-test', + CLOUDFLARE_API_TOKEN: 'token-test', + CLOUDFLARE_API_BASE_URL: 'https://api.cloudflare.test', + BUNDLE_ARTIFACTS_KV: kv, + REPO_SESSION: { + idFromName(name: string) { + repoSessionAccesses.push(name) + throw new Error('metadata-only job updates must not publish source') + }, + get() { + throw new Error('metadata-only job updates must not open repo sessions') + }, + }, + JOB_MANAGER: { + idFromName(name: string) { + return name as unknown as DurableObjectId + }, + get() { + return { + async syncAlarm(input: { userId: string }) { + return { + ok: true as const, + userId: input.userId, + nextRunAt: null, + } + }, + } + }, + }, + } as unknown as Env + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + result: { id: 'artifact-repo-1' }, + errors: [], + messages: [], + }), + { status: 200 }, + ), + ) + + try { + const codemode = await buildCodemodeFns(env, callerContext) + await expect( + codemode.job_update({ + id: job.id, + enabled: false, + }), + ).resolves.toMatchObject({ + job_id: job.id, + name: 'hrv-discord-reaction-poller', + enabled: false, + params: { + channelId: 'discord-channel-1', + }, + }) + expect(repoSessionAccesses).toEqual([]) + await expect( + db + .prepare('SELECT * FROM jobs WHERE id = ? AND user_id = ?') + .bind(job.id, callerContext.user.userId) + .first>(), + ).resolves.toMatchObject({ + enabled: 0, + }) + + await expect(codemode.job_delete({ id: job.id })).resolves.toEqual({ + job_id: job.id, + deleted: true, + }) + expect(fetchSpy).toHaveBeenCalledWith( + `https://api.cloudflare.test/client/v4/accounts/acct-test/artifacts/namespaces/default/repos/job-${job.id}`, + expect.objectContaining({ method: 'DELETE' }), + ) + await expect( + db + .prepare('SELECT * FROM jobs WHERE id = ? AND user_id = ?') + .bind(job.id, callerContext.user.userId) + .first>(), + ).resolves.toBeNull() + await expect( + db + .prepare('SELECT * FROM entity_sources WHERE id = ?') + .bind(job.sourceId) + .first>(), + ).resolves.toBeNull() + expect(kv.deletedKeys).toEqual([ + `source-snapshot:v1:${job.sourceId}:published-commit-1`, + `source-manifest-snapshot:v1:${job.sourceId}:published-commit-1`, + ]) + } finally { + fetchSpy.mockRestore() + } +}) + test('runBundledModuleWithRegistry passes params as an explicit entrypoint argument', async () => { const env = {} as Env const callerContext = createMcpCallerContext({ From ffb24774998847a2456811ff025df8487729dcac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 06:20:28 +0000 Subject: [PATCH 2/2] Address job mutation review feedback Co-authored-by: Kent C. Dodds --- packages/worker/src/jobs/service.node.test.ts | 11 +++++++ packages/worker/src/jobs/service.ts | 11 +++++-- .../mcp/run-codemode-registry.node.test.ts | 33 ++++++++++++++++--- .../repo/artifact-repo-cleanup.node.test.ts | 13 +++++--- .../worker/src/repo/artifact-repo-cleanup.ts | 12 +++++-- packages/worker/src/repo/entity-sources.ts | 14 ++++++++ 6 files changed, 79 insertions(+), 15 deletions(-) diff --git a/packages/worker/src/jobs/service.node.test.ts b/packages/worker/src/jobs/service.node.test.ts index 9b18ba0f8e..78c1d54226 100644 --- a/packages/worker/src/jobs/service.node.test.ts +++ b/packages/worker/src/jobs/service.node.test.ts @@ -376,6 +376,17 @@ function createDatabase() { row['id'] === params[0] && row['user_id'] === params[1], ) as T | null } + if ( + query.includes( + 'SELECT * FROM entity_sources WHERE id = ? AND user_id = ?', + ) + ) { + return selectOne( + 'entity_sources', + (row) => + row['id'] === params[0] && row['user_id'] === params[1], + ) as T | null + } if (query.includes('SELECT * FROM entity_sources WHERE id = ?')) { return selectOne( 'entity_sources', diff --git a/packages/worker/src/jobs/service.ts b/packages/worker/src/jobs/service.ts index 331cb417e9..88eddd6416 100644 --- a/packages/worker/src/jobs/service.ts +++ b/packages/worker/src/jobs/service.ts @@ -55,6 +55,7 @@ import { runBundledModuleWithRegistry } from '#mcp/run-codemode-registry.ts' import { deleteEntitySource, getEntitySourceById, + getEntitySourceByIdForUser, } from '#worker/repo/entity-sources.ts' import { cleanupArtifactReposForSource } from '#worker/repo/artifact-repo-cleanup.ts' import { repoSessionRpc } from '#worker/repo/repo-session-do.ts' @@ -697,10 +698,12 @@ async function cleanupAdHocJobSource(input: { if (!input.sourceId) { return } - const source = await getEntitySourceById(input.env.APP_DB, input.sourceId) + const source = await getEntitySourceByIdForUser(input.env.APP_DB, { + id: input.sourceId, + userId: input.userId, + }) if ( !source || - source.user_id !== input.userId || source.entity_kind !== 'job' || source.entity_id !== input.jobId ) { @@ -927,7 +930,9 @@ function shouldSyncJobSourceForUpdate(body: JobUpdateInput) { body.code !== undefined || body.name !== undefined || body.schedule !== undefined || - body.timezone !== undefined + body.timezone !== undefined || + body.sourceId !== undefined || + body.publishedCommit !== undefined ) } diff --git a/packages/worker/src/mcp/run-codemode-registry.node.test.ts b/packages/worker/src/mcp/run-codemode-registry.node.test.ts index 507bd6c017..f635750739 100644 --- a/packages/worker/src/mcp/run-codemode-registry.node.test.ts +++ b/packages/worker/src/mcp/run-codemode-registry.node.test.ts @@ -250,10 +250,14 @@ function createJobMutationDatabase(input: { row['id'] === params[0] && row['user_id'] === params[1], ) as T | null } - if (normalized === 'SELECT * FROM entity_sources WHERE id = ?') { + if ( + normalized === + 'SELECT * FROM entity_sources WHERE id = ? AND user_id = ?' + ) { return selectOne( 'entity_sources', - (row) => row['id'] === params[0], + (row) => + row['id'] === params[0] && row['user_id'] === params[1], ) as T | null } throw new Error(`Unsupported first query: ${query}`) @@ -504,6 +508,8 @@ test('buildCodemodeFns updates and deletes jobs through production-shaped bindin }) const kv = createJobMutationKv() const repoSessionAccesses: Array = [] + const jobManagerNames: Array = [] + const jobManagerSyncPayloads: Array<{ userId: string; source?: string }> = [] const env = { APP_DB: db, SENTRY_ENVIRONMENT: 'production', @@ -522,11 +528,18 @@ test('buildCodemodeFns updates and deletes jobs through production-shaped bindin }, JOB_MANAGER: { idFromName(name: string) { + jobManagerNames.push(name) + if (name !== callerContext.user.userId) { + throw new Error( + `Expected JOB_MANAGER to be scoped to ${callerContext.user.userId}`, + ) + } return name as unknown as DurableObjectId }, get() { return { async syncAlarm(input: { userId: string }) { + jobManagerSyncPayloads.push(input) return { ok: true as const, userId: input.userId, @@ -565,6 +578,10 @@ test('buildCodemodeFns updates and deletes jobs through production-shaped bindin }, }) expect(repoSessionAccesses).toEqual([]) + expect(jobManagerNames).toEqual([callerContext.user.userId]) + expect(jobManagerSyncPayloads).toMatchObject([ + { userId: callerContext.user.userId }, + ]) await expect( db .prepare('SELECT * FROM jobs WHERE id = ? AND user_id = ?') @@ -578,6 +595,14 @@ test('buildCodemodeFns updates and deletes jobs through production-shaped bindin job_id: job.id, deleted: true, }) + expect(jobManagerNames).toEqual([ + callerContext.user.userId, + callerContext.user.userId, + ]) + expect(jobManagerSyncPayloads).toMatchObject([ + { userId: callerContext.user.userId }, + { userId: callerContext.user.userId }, + ]) expect(fetchSpy).toHaveBeenCalledWith( `https://api.cloudflare.test/client/v4/accounts/acct-test/artifacts/namespaces/default/repos/job-${job.id}`, expect.objectContaining({ method: 'DELETE' }), @@ -590,8 +615,8 @@ test('buildCodemodeFns updates and deletes jobs through production-shaped bindin ).resolves.toBeNull() await expect( db - .prepare('SELECT * FROM entity_sources WHERE id = ?') - .bind(job.sourceId) + .prepare('SELECT * FROM entity_sources WHERE id = ? AND user_id = ?') + .bind(job.sourceId, callerContext.user.userId) .first>(), ).resolves.toBeNull() expect(kv.deletedKeys).toEqual([ diff --git a/packages/worker/src/repo/artifact-repo-cleanup.node.test.ts b/packages/worker/src/repo/artifact-repo-cleanup.node.test.ts index 1dcb597383..ebe12fc24e 100644 --- a/packages/worker/src/repo/artifact-repo-cleanup.node.test.ts +++ b/packages/worker/src/repo/artifact-repo-cleanup.node.test.ts @@ -3,6 +3,7 @@ import { expect, test, vi } from 'vitest' const mockModule = vi.hoisted(() => ({ deleteArtifactRepo: vi.fn(), getEntitySourceById: vi.fn(), + getEntitySourceByIdForUser: vi.fn(), listEntitySourcesByUser: vi.fn(), listRepoSessionsBySource: vi.fn(async () => []), listRepoSessionsByUser: vi.fn(async () => []), @@ -20,6 +21,8 @@ vi.mock('./artifacts.ts', () => ({ vi.mock('./entity-sources.ts', () => ({ getEntitySourceById: (...args: Array) => mockModule.getEntitySourceById(...args), + getEntitySourceByIdForUser: (...args: Array) => + mockModule.getEntitySourceByIdForUser(...args), listEntitySourcesByUser: (...args: Array) => mockModule.listEntitySourcesByUser(...args), })) @@ -70,7 +73,7 @@ test('artifact repo cleanup deletes scoped repos and records warning-only failur }), ).toBe(true) - mockModule.getEntitySourceById.mockResolvedValue({ + mockModule.getEntitySourceByIdForUser.mockResolvedValue({ id: 'source-1', user_id: 'user-1', repo_id: 'package-pkg-1', @@ -112,7 +115,7 @@ test('artifact repo cleanup deletes scoped repos and records warning-only failur ).toBe(2) mockModule.listRepoSessionsByUser.mockResolvedValue([]) - mockModule.getEntitySourceById.mockResolvedValue({ + mockModule.getEntitySourceByIdForUser.mockResolvedValue({ id: 'source-1', user_id: 'user-other', repo_id: 'package-pkg-1', @@ -152,7 +155,7 @@ test('generic source cleanup deletes the source root with user scope checks', as id: 'repo-deleted', alreadyDeleted: false, }) - mockModule.getEntitySourceById.mockResolvedValue({ + mockModule.getEntitySourceByIdForUser.mockResolvedValue({ id: 'source-1', user_id: 'user-1', repo_id: 'job-job-1', @@ -170,7 +173,7 @@ test('generic source cleanup deletes the source root with user scope checks', as expect(mockModule.deleteArtifactRepo).toHaveBeenCalledWith('job-job-1') mockModule.deleteArtifactRepo.mockClear() - mockModule.getEntitySourceById.mockResolvedValue({ + mockModule.getEntitySourceByIdForUser.mockResolvedValue({ id: 'source-1', user_id: 'user-2', repo_id: 'job-job-1', @@ -191,7 +194,7 @@ test('generic source cleanup deletes the source root with user scope checks', as expect(warnings).toHaveLength(1) mockModule.hasArtifactsAccess.mockReturnValue(false) - mockModule.getEntitySourceById.mockResolvedValue({ + mockModule.getEntitySourceByIdForUser.mockResolvedValue({ id: 'source-1', user_id: 'user-1', repo_id: 'job-job-1', diff --git a/packages/worker/src/repo/artifact-repo-cleanup.ts b/packages/worker/src/repo/artifact-repo-cleanup.ts index 643225f8f6..8164bd8b2a 100644 --- a/packages/worker/src/repo/artifact-repo-cleanup.ts +++ b/packages/worker/src/repo/artifact-repo-cleanup.ts @@ -4,7 +4,7 @@ import { type ArtifactDeleteRepoResult, } from './artifacts.ts' import { - getEntitySourceById, + getEntitySourceByIdForUser, listEntitySourcesByUser, } from './entity-sources.ts' import { @@ -120,7 +120,10 @@ export async function cleanupArtifactReposForPackage(input: { sourceId: string warnings?: Array }) { - const source = await getEntitySourceById(input.env.APP_DB, input.sourceId) + const source = await getEntitySourceByIdForUser(input.env.APP_DB, { + id: input.sourceId, + userId: input.userId, + }) if (!source) return 0 if (source.user_id !== input.userId) { input.warnings?.push( @@ -161,7 +164,10 @@ export async function cleanupArtifactReposForSource(input: { deleted: number artifactAccessUnavailable: boolean }> { - const source = await getEntitySourceById(input.env.APP_DB, input.sourceId) + const source = await getEntitySourceByIdForUser(input.env.APP_DB, { + id: input.sourceId, + userId: input.userId, + }) if (!source) return { deleted: 0, artifactAccessUnavailable: false } if (source.user_id !== input.userId) { input.warnings?.push( diff --git a/packages/worker/src/repo/entity-sources.ts b/packages/worker/src/repo/entity-sources.ts index eb3a5398e1..56f6c97418 100644 --- a/packages/worker/src/repo/entity-sources.ts +++ b/packages/worker/src/repo/entity-sources.ts @@ -65,6 +65,20 @@ export async function getEntitySourceById( return result ? mapEntitySourceRow(result) : null } +export async function getEntitySourceByIdForUser( + db: D1Database, + input: { + id: string + userId: string + }, +): Promise { + const result = await db + .prepare(`SELECT * FROM entity_sources WHERE id = ? AND user_id = ?`) + .bind(input.id, input.userId) + .first>() + return result ? mapEntitySourceRow(result) : null +} + export async function getEntitySourceByEntity( db: D1Database, input: {