(params: {
tx: DbOrTx
sourceWorkspaceId: string
childWorkspaceId: string
userId: string
fileIds?: string[]
fileKeys?: string[]
now: Date
})
| 66 | * least one of the two must be non-empty; both may be supplied (their matched rows union). |
| 67 | */ |
| 68 | export async function planForkFileCopies(params: { |
| 69 | tx: DbOrTx |
| 70 | sourceWorkspaceId: string |
| 71 | childWorkspaceId: string |
| 72 | userId: string |
| 73 | fileIds?: string[] |
| 74 | fileKeys?: string[] |
| 75 | now: Date |
| 76 | }): Promise<PlanForkFileCopiesResult> { |
| 77 | const { tx, sourceWorkspaceId, childWorkspaceId, userId, now } = params |
| 78 | const fileIds = params.fileIds ?? [] |
| 79 | const fileKeys = params.fileKeys ?? [] |
| 80 | const keyMap = new Map<string, string>() |
| 81 | const idMap = new Map<string, string>() |
| 82 | const blobTasks: BlobCopyTask[] = [] |
| 83 | if (fileIds.length === 0 && fileKeys.length === 0) return { keyMap, idMap, blobTasks } |
| 84 | |
| 85 | // Match by id and/or storage key (OR'd) so either selection shape resolves to the same |
| 86 | // source rows. Batch the metadata read (one query for all selected files): non-deleted, |
| 87 | // scoped to the source workspace, and restricted to durable `workspace` files. Only |
| 88 | // workspace files are forkable - chat/copilot/mothership uploads are session-scoped and |
| 89 | // their chat-bound unique index can't be duplicated - so any non-workspace id/key passed |
| 90 | // here is ignored rather than copied. |
| 91 | const selectors = [ |
| 92 | fileIds.length > 0 ? inArray(workspaceFiles.id, fileIds) : undefined, |
| 93 | fileKeys.length > 0 ? inArray(workspaceFiles.key, fileKeys) : undefined, |
| 94 | ].filter((clause): clause is NonNullable<typeof clause> => clause !== undefined) |
| 95 | const metas = await tx |
| 96 | .select() |
| 97 | .from(workspaceFiles) |
| 98 | .where( |
| 99 | and( |
| 100 | selectors.length === 1 ? selectors[0] : or(...selectors), |
| 101 | eq(workspaceFiles.workspaceId, sourceWorkspaceId), |
| 102 | eq(workspaceFiles.context, 'workspace'), |
| 103 | isNull(workspaceFiles.deletedAt) |
| 104 | ) |
| 105 | ) |
| 106 | |
| 107 | for (const meta of metas) { |
| 108 | const childFileId = generateId() |
| 109 | // Use the canonical workspace-file key (`workspace/{id}/...`) so the file-serve |
| 110 | // API can infer the storage context; a bare `{id}/...` key has no context prefix. |
| 111 | const targetKey = generateWorkspaceFileKey(childWorkspaceId, meta.originalName) |
| 112 | await tx.insert(workspaceFiles).values({ |
| 113 | ...meta, |
| 114 | id: childFileId, |
| 115 | key: targetKey, |
| 116 | workspaceId: childWorkspaceId, |
| 117 | userId, |
| 118 | folderId: null, |
| 119 | deletedAt: null, |
| 120 | uploadedAt: now, |
| 121 | }) |
| 122 | keyMap.set(meta.key, targetKey) |
| 123 | idMap.set(meta.id, childFileId) |
| 124 | blobTasks.push({ |
| 125 | sourceKey: meta.key, |
no test coverage detected