@@ -46,7 +46,11 @@ import { RECORD_PAGE_ATTACHMENT_ROWS, recordAttachmentsCountField } from './reco
4646 *
4747 * Storage model: one `sys_file` row per uploaded blob (three-step
4848 * presigned upload via @object-ui/data-objectstack's ObjectStack adapter), one
49- * `sys_attachment` join row linking it to `(parent_object, parent_id)`.
49+ * `sys_attachment` join row linking it to `(parent_object, parent_id)`. The
50+ * join row is written by the upload's own completion (`attachTo`), as the
51+ * signed-in user: the server's attach verb, never a `sys_attachment` create
52+ * through the generic data door, which a member who may attach files but not
53+ * read `sys_file` is refused (`reference_not_found` on `file_id`).
5054 * Downloads fetch a short-lived signed URL from `/storage/files/:fileId/url`
5155 * with the console's Bearer token (the endpoint requires an authenticated
5256 * session for attachments-scope files, objectstack-ai/objectstack#2970), then open it.
@@ -65,9 +69,9 @@ interface AttachmentRow {
6569export interface RecordAttachmentsPanelProps {
6670 objectName : string ;
6771 recordId : string ;
68- /** ObjectUI DataSource (sys_attachment CRUD goes through the generic data path). */
72+ /** ObjectUI DataSource (the list read and the delete go through the generic data path; the attach does not ). */
6973 dataSource : any ;
70- /** Current user id, stamped on `uploaded_by`. */
74+ /** Current user id. The server stamps `uploaded_by` from the session; the panel no longer sends it . */
7175 currentUserId ?: string | null ;
7276 className ?: string ;
7377}
@@ -175,7 +179,6 @@ export const RecordAttachmentsPanel: React.FC<RecordAttachmentsPanelProps> = ({
175179 objectName,
176180 recordId,
177181 dataSource,
178- currentUserId,
179182 className,
180183} ) => {
181184 const { t } = useObjectTranslation ( ) ;
@@ -216,10 +219,10 @@ export const RecordAttachmentsPanel: React.FC<RecordAttachmentsPanelProps> = ({
216219 * and a delete go through the storage route, not `sys_attachment`'s generic
217220 * data door, so that door's operation set says nothing about them.
218221 *
219- * Upload needs `create`. Without it the three-step presigned flow below
220- * commits a `sys_file` and only the final `sys_attachment` insert is
221- * refused: the user meets the error after the bytes are stored, and the file
222- * stays committed with nothing attached to it .
222+ * Upload needs `create`. Without it the attach at the end of the upload
223+ * below is refused after the bytes are stored ( the server leaves the file
224+ * pending, and its retention reclaims it); hiding Upload spares the user
225+ * that round trip .
223226 *
224227 * Delete needs `delete`, and being the uploader does not change that. The
225228 * server asks the object grant first (plugin-security's CRUD check wraps the
@@ -252,9 +255,17 @@ export const RecordAttachmentsPanel: React.FC<RecordAttachmentsPanelProps> = ({
252255 // upload adapter and the download-URL fetch — the storage routes require a
253256 // session and there is no cookie for `credentials: 'include'` to carry.
254257 const authFetch = React . useMemo ( ( ) => createAuthenticatedFetch ( ) , [ ] ) ;
258+ // Every upload from this panel is attached to this record by the call that
259+ // completes it (`attachTo`): the server's attach verb, run as the user.
255260 const adapter = React . useMemo (
256- ( ) => createObjectStackUploadAdapter ( { baseUrl, scope : 'attachments' , fetchImpl : authFetch } ) ,
257- [ baseUrl , authFetch ] ,
261+ ( ) =>
262+ createObjectStackUploadAdapter ( {
263+ baseUrl,
264+ scope : 'attachments' ,
265+ fetchImpl : authFetch ,
266+ ...( objectName && recordId ? { attachTo : { parentObject : objectName , parentId : recordId } } : { } ) ,
267+ } ) ,
268+ [ baseUrl , authFetch , objectName , recordId ] ,
258269 ) ;
259270
260271 /** Map the server's fail-closed 40x codes (objectstack-ai/objectstack#2755, objectstack-ai/objectstack#2970) to friendly copy. */
@@ -390,27 +401,18 @@ export const RecordAttachmentsPanel: React.FC<RecordAttachmentsPanelProps> = ({
390401
391402 const handleFiles = React . useCallback (
392403 async ( files : FileList | null ) => {
393- if ( ! files || files . length === 0 || ! dataSource ) return ;
404+ if ( ! files || files . length === 0 || ! dataSource || ! objectName || ! recordId ) return ;
394405 setUploading ( true ) ;
395406 setError ( null ) ;
396407 try {
397408 for ( const file of Array . from ( files ) ) {
398- // 1) blob → sys_file via the canonical presigned three-step flow
409+ // blob → sys_file → sys_attachment: the presigned three-step flow,
410+ // whose completion attaches the file to this record as the user
411+ // (the server checks the create grant, edit access to the record,
412+ // `enable.files`, and that the file is the user's own upload).
399413 const uploaded = await adapter . upload ( file ) ;
400- const fileId = ( uploaded . meta as any ) ?. fileId as string | undefined ;
401- if ( ! fileId ) throw new Error ( 'Upload did not return a fileId' ) ;
402- // 2) join row → sys_attachment (server enforces enable.files)
403- await dataSource . create ( 'sys_attachment' , {
404- parent_object : objectName ,
405- parent_id : recordId ,
406- file_id : fileId ,
407- file_name : uploaded . name ?? file . name ,
408- mime_type : uploaded . mimeType ?? file . type ,
409- size : uploaded . size ?? file . size ,
410- // Back-compat with pre-objectstack-ai/objectstack#2755 servers; a current server stamps
411- // `uploaded_by` from the session and ignores this value.
412- ...( currentUserId ? { uploaded_by : currentUserId } : { } ) ,
413- } ) ;
414+ const attachmentId = ( uploaded . meta as any ) ?. attachmentId as string | undefined ;
415+ if ( ! attachmentId ) throw new Error ( 'The server completed the upload but did not attach it to this record' ) ;
414416 }
415417 await refresh ( ) ;
416418 } catch ( err : any ) {
@@ -420,7 +422,7 @@ export const RecordAttachmentsPanel: React.FC<RecordAttachmentsPanelProps> = ({
420422 if ( inputRef . current ) inputRef . current . value = '' ;
421423 }
422424 } ,
423- [ adapter , dataSource , objectName , recordId , currentUserId , refresh ] ,
425+ [ adapter , dataSource , objectName , recordId , refresh ] ,
424426 ) ;
425427
426428 /**
0 commit comments