chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,63 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetWorkspaceFile, mockGetFileMetadataByKey } = vi.hoisted(() => ({
mockGetWorkspaceFile: vi.fn(),
mockGetFileMetadataByKey: vi.fn(),
}))
vi.mock('@/lib/uploads/contexts/workspace', () => ({ getWorkspaceFile: mockGetWorkspaceFile }))
vi.mock('@/lib/uploads/server/metadata', () => ({ getFileMetadataByKey: mockGetFileMetadataByKey }))
import { resolveWorkspaceInlineImage } from '@/lib/uploads/server/inline-image'
describe('resolveWorkspaceInlineImage', () => {
beforeEach(() => vi.clearAllMocks())
it('resolves by fileId scoped to the workspace (getWorkspaceFile already enforces scope)', async () => {
mockGetWorkspaceFile.mockResolvedValue({
key: 'workspace/ws-1/x.png',
type: 'image/png',
name: 'x.png',
})
const out = await resolveWorkspaceInlineImage('ws-1', { fileId: 'wf_a' })
expect(mockGetWorkspaceFile).toHaveBeenCalledWith('ws-1', 'wf_a')
expect(out).toEqual({
key: 'workspace/ws-1/x.png',
contentType: 'image/png',
filename: 'x.png',
})
})
it('returns null when getWorkspaceFile finds nothing (cross-workspace / deleted / non-workspace)', async () => {
mockGetWorkspaceFile.mockResolvedValue(null)
expect(await resolveWorkspaceInlineImage('ws-1', { fileId: 'wf_a' })).toBeNull()
})
it('resolves by key only when the row belongs to the workspace', async () => {
mockGetFileMetadataByKey.mockResolvedValue({
key: 'workspace/ws-1/x.png',
workspaceId: 'ws-1',
contentType: 'image/png',
originalName: 'x.png',
})
const out = await resolveWorkspaceInlineImage('ws-1', { key: 'workspace/ws-1/x.png' })
expect(out).toEqual({
key: 'workspace/ws-1/x.png',
contentType: 'image/png',
filename: 'x.png',
})
})
it('returns null when the keyed row belongs to a different workspace', async () => {
mockGetFileMetadataByKey.mockResolvedValue({
key: 'workspace/ws-2/x.png',
workspaceId: 'ws-2',
contentType: 'image/png',
originalName: 'x.png',
})
expect(await resolveWorkspaceInlineImage('ws-1', { key: 'workspace/ws-2/x.png' })).toBeNull()
})
})
@@ -0,0 +1,41 @@
import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace'
import { getFileMetadataByKey } from '@/lib/uploads/server/metadata'
/**
* A markdown-embedded image reference: either a workspace file `fileId` (view-URL embeds) or a
* workspace storage `key` (serve-URL embeds). Exactly one is set — enforced at the route boundary.
*/
export interface InlineImageRef {
key?: string
fileId?: string
}
/** The fields a serve handler needs to return an embedded image. */
export interface ResolvedInlineImage {
key: string
contentType: string
filename: string
}
/**
* Resolve an embedded-image reference to its storage key + metadata, **scoped to `workspaceId`**.
* Returns null whenever the reference is not a live `workspace` file in that workspace — a
* cross-workspace, non-workspace, missing, or deleted file. This is the single workspace-scope gate
* shared by the in-app inline route and the public-share cascade, mirroring how the user-facing file
* view resolves a file within its workspace ({@link getWorkspaceFile}).
*/
export async function resolveWorkspaceInlineImage(
workspaceId: string,
ref: InlineImageRef
): Promise<ResolvedInlineImage | null> {
if (ref.fileId) {
const file = await getWorkspaceFile(workspaceId, ref.fileId)
return file ? { key: file.key, contentType: file.type, filename: file.name } : null
}
if (ref.key) {
const record = await getFileMetadataByKey(ref.key, 'workspace')
if (!record || record.workspaceId !== workspaceId) return null
return { key: record.key, contentType: record.contentType, filename: record.originalName }
}
return null
}
+277
View File
@@ -0,0 +1,277 @@
import { db } from '@sim/db'
import { workspaceFiles } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import type { StorageContext } from '../shared/types'
const logger = createLogger('FileMetadata')
export type FileMetadataRecord = typeof workspaceFiles.$inferSelect
export interface FileMetadataInsertOptions {
key: string
userId: string
workspaceId?: string | null
context: StorageContext
originalName: string
contentType: string
size: number
folderId?: string | null
/** Optional — a UUID is generated when omitted. */
id?: string
}
/**
* Insert file metadata into workspaceFiles table
* Handles duplicate key errors gracefully by returning existing record
*/
export async function insertFileMetadata(
options: FileMetadataInsertOptions
): Promise<FileMetadataRecord> {
const { key, userId, workspaceId, context, originalName, contentType, size, folderId, id } =
options
const existingDeleted = await db
.select()
.from(workspaceFiles)
.where(eq(workspaceFiles.key, key))
.limit(1)
if (existingDeleted.length > 0 && existingDeleted[0].deletedAt) {
const [restored] = await db
.update(workspaceFiles)
.set({
userId,
workspaceId: workspaceId || null,
folderId: folderId ?? null,
context,
originalName,
displayName: originalName,
contentType,
size,
deletedAt: null,
uploadedAt: new Date(),
})
.where(eq(workspaceFiles.id, existingDeleted[0].id))
.returning()
if (restored) {
return restored
}
}
const existing = await db
.select()
.from(workspaceFiles)
.where(and(eq(workspaceFiles.key, key), isNull(workspaceFiles.deletedAt)))
.limit(1)
if (existing.length > 0) {
return existing[0]
}
const fileId = id || generateId()
try {
const [inserted] = await db
.insert(workspaceFiles)
.values({
id: fileId,
key,
userId,
workspaceId: workspaceId || null,
folderId: folderId ?? null,
context,
originalName,
displayName: originalName,
contentType,
size,
deletedAt: null,
uploadedAt: new Date(),
})
.returning()
return inserted
} catch (error) {
const code = (error as { code?: string } | null)?.code
if (code === '23505' || (error instanceof Error && error.message.includes('unique'))) {
const existingAfterError = await db
.select()
.from(workspaceFiles)
.where(and(eq(workspaceFiles.key, key), isNull(workspaceFiles.deletedAt)))
.limit(1)
if (existingAfterError.length > 0) {
return existingAfterError[0]
}
}
logger.error(`Failed to insert file metadata for key: ${key}`, error)
throw error
}
}
/**
* Bulk-insert file metadata rows in a single statement.
*
* Intended for batch upload flows that create many fresh keys at once (e.g. the
* presigned batch route), replacing a fan-out of individual `insertFileMetadata`
* calls. Uses `ON CONFLICT DO NOTHING` on the active-key unique index, so it is
* safe against a concurrent single insert and idempotent for already-present
* active keys. Unlike {@link insertFileMetadata} it does NOT restore
* soft-deleted rows — callers use this only for newly generated keys.
*/
export async function insertFileMetadataMany(
rows: Array<Omit<FileMetadataInsertOptions, 'id'> & { id?: string }>
): Promise<void> {
if (rows.length === 0) {
return
}
await db
.insert(workspaceFiles)
.values(
rows.map((row) => ({
id: row.id || generateId(),
key: row.key,
userId: row.userId,
workspaceId: row.workspaceId || null,
folderId: row.folderId ?? null,
context: row.context,
originalName: row.originalName,
displayName: row.originalName,
contentType: row.contentType,
size: row.size,
deletedAt: null,
uploadedAt: new Date(),
}))
)
.onConflictDoNothing()
}
/**
* Get file metadata by key with optional context filter
*/
export async function getFileMetadataByKey(
key: string,
context?: StorageContext,
options?: { includeDeleted?: boolean }
): Promise<FileMetadataRecord | null> {
const { includeDeleted = false } = options ?? {}
const conditions = [eq(workspaceFiles.key, key)]
if (context) {
conditions.push(eq(workspaceFiles.context, context))
}
if (!includeDeleted) {
conditions.push(isNull(workspaceFiles.deletedAt))
}
const [record] = await db
.select()
.from(workspaceFiles)
.where(conditions.length > 1 ? and(...conditions) : conditions[0])
// Prefer the active row when includeDeleted lets both an active and a
// soft-deleted row for the same key match.
.orderBy(sql`${workspaceFiles.deletedAt} IS NULL DESC`)
.limit(1)
return record ?? null
}
/**
* Get active (non-deleted) file metadata for multiple keys in a single query.
* Batches what would otherwise be N `getFileMetadataByKey` calls.
*/
export async function getFileMetadataByKeys(
keys: string[],
context: StorageContext,
executor: Pick<typeof db, 'select'> = db
): Promise<FileMetadataRecord[]> {
if (keys.length === 0) {
return []
}
return executor
.select()
.from(workspaceFiles)
.where(
and(
inArray(workspaceFiles.key, keys),
eq(workspaceFiles.context, context),
isNull(workspaceFiles.deletedAt)
)
)
}
/**
* Get file metadata by ID
*/
export async function getFileMetadataById(
id: string,
options?: { includeDeleted?: boolean }
): Promise<FileMetadataRecord | null> {
const { includeDeleted = false } = options ?? {}
const conditions = [eq(workspaceFiles.id, id)]
if (!includeDeleted) conditions.push(isNull(workspaceFiles.deletedAt))
const [record] = await db
.select()
.from(workspaceFiles)
.where(conditions.length > 1 ? and(...conditions) : conditions[0])
.limit(1)
return record ?? null
}
/**
* Delete file metadata by key
*/
export async function deleteFileMetadata(key: string): Promise<boolean> {
await db
.update(workspaceFiles)
.set({ deletedAt: new Date() })
.where(and(eq(workspaceFiles.key, key), isNull(workspaceFiles.deletedAt)))
return true
}
/**
* Fields needed to record a trusted storage-key -> workspace ownership binding
* for a knowledge-base file. The `context` is always `'knowledge-base'`, so it is
* not part of this shape.
*/
export interface KnowledgeBaseFileOwnership {
key: string
userId: string
workspaceId: string
originalName: string
contentType: string
size: number
}
/**
* Record the ownership binding for a single knowledge-base upload. KB file
* authorization (`verifyKBFileAccess`) resolves the owning workspace from this
* binding, so every KB object must have exactly one. Single source of truth for
* the binding shape across the presigned, batch-presigned, and multipart upload
* paths — keep all callers routed through here so they cannot drift.
*/
export async function recordKnowledgeBaseFileOwnership(
ownership: KnowledgeBaseFileOwnership
): Promise<void> {
await insertFileMetadata({ ...ownership, context: 'knowledge-base' })
}
/**
* Bulk variant of {@link recordKnowledgeBaseFileOwnership} for batch upload flows.
* Idempotent against the active-key unique index (ON CONFLICT DO NOTHING).
*/
export async function recordKnowledgeBaseFileOwnershipMany(
ownerships: KnowledgeBaseFileOwnership[]
): Promise<void> {
if (ownerships.length === 0) {
return
}
await insertFileMetadataMany(
ownerships.map((ownership) => ({ ...ownership, context: 'knowledge-base' }))
)
}