Skip to content

Commit 18188ce

Browse files
committed
fix(files): resolve metadata version from the record's own storage key
1 parent 2c65f4b commit 18188ce

4 files changed

Lines changed: 34 additions & 26 deletions

File tree

‎apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox'
3939
import {
4040
getCurrentWorkspaceFileVersion,
41+
getWorkspaceFileVersionNumberForRecord,
4142
queryWorkspaceFileVersions,
4243
releaseWorkspaceFileVersionsForPurgeInTx,
4344
} from '@/lib/uploads/contexts/workspace/workspace-file-versions'
@@ -227,6 +228,8 @@ describe('workspace file version history in PostgreSQL', () => {
227228
const after = await getWorkspaceFile(fixture.workspaceId, fixture.fileId)
228229
if (!after) throw new Error('file missing')
229230
expect((await getCurrentWorkspaceFileVersion(after)).version).toBe(2)
231+
expect(await getWorkspaceFileVersionNumberForRecord(before)).toBe(1)
232+
expect(await getWorkspaceFileVersionNumberForRecord(after)).toBe(2)
230233
})
231234

232235
it('does not keep an empty shell as a version of its own', async () => {

‎apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,24 @@ export async function getCurrentWorkspaceFileVersion(
497497
return head ? toVersionRecord(head) : implicitFirstVersion(file)
498498
}
499499

500+
/**
501+
* The version number of the content a file record describes. Matched by the record's storage key, so
502+
* it stays exact even if a write committed after the record was read: that record's key still names
503+
* its own version row. A key with no row is either a file with no history (implicit version 1) or
504+
* bytes a collaborative write replaced within the same version, which keeps the head's number.
505+
*/
506+
export async function getWorkspaceFileVersionNumberForRecord(
507+
file: Pick<WorkspaceFileVersionSubject, 'id' | 'key'>
508+
): Promise<number> {
509+
const [row] = await db
510+
.select({ version: workspaceFileVersion.version })
511+
.from(workspaceFileVersion)
512+
.where(and(eq(workspaceFileVersion.fileId, file.id), eq(workspaceFileVersion.key, file.key)))
513+
.limit(1)
514+
if (row) return row.version
515+
return (await loadWorkspaceFileVersionHead(file.id))?.version ?? 1
516+
}
517+
500518
/** One version of a file, or null when it never existed or retention removed it. */
501519
export async function getWorkspaceFileVersion(
502520
file: WorkspaceFileVersionSubject,

‎apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ const mocks = vi.hoisted(() => ({
88
getWorkspaceFile: vi.fn(),
99
getShareForResource: vi.fn(),
1010
resolvePermission: vi.fn(),
11-
getCurrentVersion: vi.fn(),
11+
getVersionNumberForRecord: vi.fn(),
1212
}))
1313

1414
vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({
15-
getCurrentWorkspaceFileVersion: mocks.getCurrentVersion,
15+
getWorkspaceFileVersionNumberForRecord: mocks.getVersionNumberForRecord,
1616
}))
1717

1818
vi.mock('@sim/platform-authz/workspace', () => ({
@@ -177,19 +177,16 @@ describe('readWorkspaceFileMetadataWithVersion', () => {
177177
mocks.resolvePermission.mockResolvedValue('admin')
178178
})
179179

180-
it('pairs the version number with the stored object the returned record describes', async () => {
181-
const rewritten = { ...file, key: 'workspace/ws/data-2.csv', size: 50 }
182-
mocks.getWorkspaceFile.mockResolvedValueOnce(file).mockResolvedValueOnce(rewritten)
183-
mocks.getCurrentVersion
184-
.mockResolvedValueOnce({ version: 2, key: rewritten.key })
185-
.mockResolvedValueOnce({ version: 2, key: rewritten.key })
180+
it('reports the version of the stored object the returned record describes', async () => {
181+
mocks.getWorkspaceFile.mockResolvedValueOnce(file)
182+
mocks.getVersionNumberForRecord.mockResolvedValueOnce(4)
186183

187184
await expect(
188185
readWorkspaceFileMetadataWithVersion.execute({
189186
principal,
190187
input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' },
191188
})
192-
).resolves.toEqual({ file: rewritten, share, currentVersion: 2 })
193-
expect(mocks.getWorkspaceFile).toHaveBeenCalledTimes(2)
189+
).resolves.toEqual({ file, share, currentVersion: 4 })
190+
expect(mocks.getVersionNumberForRecord).toHaveBeenCalledWith(file)
194191
})
195192
})

‎apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts‎

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
getWorkspaceFile,
88
type WorkspaceFileRecord,
99
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
10-
import { getCurrentWorkspaceFileVersion } from '@/lib/uploads/contexts/workspace/workspace-file-versions'
10+
import { getWorkspaceFileVersionNumberForRecord } from '@/lib/uploads/contexts/workspace/workspace-file-versions'
1111
import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case'
1212
import { fileOperations } from '@/lib/workspace-files/application/operations'
1313
import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context'
@@ -59,28 +59,18 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({
5959
execute: executeReadWorkspaceFileMetadata,
6060
})
6161

62-
/** Attempts to read a file record and version head that describe the same stored object. */
63-
const CURRENT_VERSION_READ_ATTEMPTS = 3
64-
6562
/**
6663
* The same read plus the current version number, for the public metadata surface. Kept separate so
67-
* the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query.
68-
*
69-
* The file row and the version head are separate reads, so a content write committing between them
70-
* would pair the old key and size with the new number; the read repeats until both describe the
71-
* same stored object.
64+
* the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. The number is
65+
* resolved from the returned record's own storage key, so it always identifies the content that
66+
* record describes.
7267
*/
7368
export const readWorkspaceFileMetadataWithVersion = defineAuthorizedWorkspaceFileUseCase({
7469
operation: fileOperations.readMetadata,
7570
resolveContext: ({ input }: { input: ReadWorkspaceFileMetadataInput }) =>
7671
resolveActiveWorkspaceFileContext(input),
7772
async execute(args): Promise<ReadWorkspaceFileMetadataWithVersionResult> {
78-
for (let attempt = 1; ; attempt++) {
79-
const result = await executeReadWorkspaceFileMetadata(args)
80-
const current = await getCurrentWorkspaceFileVersion(result.file)
81-
if (current.key === result.file.key || attempt === CURRENT_VERSION_READ_ATTEMPTS) {
82-
return { ...result, currentVersion: current.version }
83-
}
84-
}
73+
const result = await executeReadWorkspaceFileMetadata(args)
74+
return { ...result, currentVersion: await getWorkspaceFileVersionNumberForRecord(result.file) }
8575
},
8676
})

0 commit comments

Comments
 (0)