feat(file): include public share status in File read output (#5191)

* feat(file): include public share status in File read output

The read operation now attaches each workspace file's public share status as a
"share" field (the share record, or null when not shared), batch-fetched via
getSharesForResources to avoid N+1. Picker/upload input files have no canonical
id and carry share: null.

* refactor(file): read share status uses visibility vocabulary, no row internals

Read's per-file "share" is now { visibility, url, allowedEmails } using the
same visibility vocabulary as Manage Sharing: 'private' when not shared (url
null, no config) instead of null, otherwise public/password/email/sso with the
link. Drops row internals (id, token, resourceType, resourceId, isActive,
hasPassword).

* fix(file): mark picker files private without a share lookup

Input (picker/upload) files only have a synthetic id (storage key/URL), so
looking them up in the shares map could collide with a canonical file id and
attach the wrong share. Give them an explicit private share instead.

* docs(file): terser share-status output descriptions

* chore(file): drop verbose comment in read share enrichment
This commit is contained in:
Theodore Li
2026-06-24 02:06:31 -04:00
committed by GitHub
parent 4554df9f77
commit 8c5da02761
2 changed files with 21 additions and 2 deletions
+20 -1
View File
@@ -17,6 +17,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { isSupportedFileType, parseBuffer } from '@/lib/file-parsers'
import {
getShareForResource,
getSharesForResources,
ShareValidationError,
upsertFileShare,
} from '@/lib/public-shares/share-manager'
@@ -417,12 +418,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const shares = await getSharesForResources('file', selectedFileIds)
const privateReadShare = () => ({
visibility: 'private' as const,
url: null,
allowedEmails: [] as string[],
})
const toReadShare = (fileId: string) => {
const share = shares.get(fileId)
if (!share || !share.isActive) return privateReadShare()
return {
visibility: share.authType,
url: share.url,
allowedEmails: share.allowedEmails,
}
}
const userFiles = files
.map((file) => workspaceFileToUserFile(file))
.filter((file): file is NonNullable<ReturnType<typeof workspaceFileToUserFile>> =>
Boolean(file)
)
.concat(selectedInputFiles)
.map((file) => ({ ...file, share: toReadShare(file.id) }))
// Picker/upload entries have only a synthetic id (storage key/URL), so they
// never carry a canonical share — mark them private without a lookup.
.concat(selectedInputFiles.map((file) => ({ ...file, share: privateReadShare() })))
logger.info('Files retrieved', {
count: userFiles.length,
+1 -1
View File
@@ -1361,7 +1361,7 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
files: {
type: 'file[]',
description:
'Workspace file objects (read), fetched file objects (fetch), the compressed archive (compress), or extracted files (decompress)',
'Workspace file objects with share status (read), fetched file objects (fetch), the compressed archive (compress), or extracted files (decompress)',
},
contents: {
type: 'array',