> For the complete documentation index, see [llms.txt](https://docs.pyck.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.pyck.cloud/graphql-api/mutations/finalize-file-upload.md).

# Finalize File Upload

finalizeFileUpload Mutation

Audience: Programmer

Finalizes a file upload by reading the actual object size from S3 storage and persisting it on the File record. Call this mutation after the client has successfully uploaded the bytes to the pre-signed URL returned by [`createFile`](/graphql-api/mutations/createfile.md).

The mutation is idempotent: if the stored `size` already matches the size reported by S3, it returns the file as-is and does not emit a redundant mutation event.

## Arguments

| Name | Type  | Description                                                        |
| ---- | ----- | ------------------------------------------------------------------ |
| `id` | `ID!` | The unique identifier of the file whose upload should be finalized |

## Return Type

`File!` - The finalized File object with the actual `size` populated from object storage.

## Typical Upload Workflow

The recommended file upload flow in pyck is:

1. **Create the file record** with [`createFile`](/graphql-api/mutations/createfile.md). Since the `size` field on `CreateFileInput` is now optional, callers can defer providing it until after the upload completes. The mutation returns a `preSignedUploadUrl` and the new `File` object.
2. **Upload the bytes** directly to S3 using HTTP `PUT` against the `preSignedUploadUrl` returned by `createFile`.
3. **Finalize the upload** by calling `finalizeFileUpload` with the file's `id`. The resolver issues a `StatObject` call against the bucket, reads the actual byte size, and stores it on the `File` record.

This split keeps the file metadata write path (`createFile`) decoupled from the data plane (S3 upload), which is essential for workers and background processes that record file metadata before the payload is available.

## Breaking Change: `size` is now Optional

To support this two-phase workflow, the following fields changed from non-null to nullable:

| Location          | Field  | Old Type | New Type |
| ----------------- | ------ | -------- | -------- |
| `CreateFileInput` | `size` | `Int!`   | `Int`    |
| `File`            | `size` | `Int!`   | `Int`    |

Existing clients that relied on `size` always being present must now handle a nullable value. After `finalizeFileUpload` succeeds, `File.size` is guaranteed to be populated.

A pair of new predicates was added to [`FileWhereInput`](#filtering-files-by-finalization-state) so callers can filter for records that have not yet been finalized:

| Predicate    | Type      | Description                                       |
| ------------ | --------- | ------------------------------------------------- |
| `sizeIsNil`  | `Boolean` | Match files whose size has not been populated yet |
| `sizeNotNil` | `Boolean` | Match files whose size has been populated         |

## Usage Example

```graphql
mutation FinalizeFileUpload($id: ID!) {
  finalizeFileUpload(id: $id) {
    id
    name
    size
    contentType
    publicURL
    updatedAt
  }
}
```

### End-to-end Workflow

```graphql
# Step 1: Create the file record without a size.
mutation {
  createFile(
    input: {
      refid: "550e8400-e29b-41d4-a716-446655440000"
      reftype: item
      filename: "product_image.jpg"
      contentType: "image/jpeg"
      description: "Product image for item"
    }
  ) {
    id
    preSignedUploadUrl
    file {
      id
      filename
      size
    }
  }
}
```

```bash
# Step 2: Upload the bytes directly to S3.
curl -X PUT --data-binary @product_image.jpg "<preSignedUploadUrl>"
```

```graphql
# Step 3: Persist the actual size on the File record.
mutation {
  finalizeFileUpload(id: "550e8400-e29b-41d4-a716-446655440000") {
    id
    size
  }
}
```

## JavaScript Example

```javascript
import { gql } from "@apollo/client";

const CREATE_FILE = gql`
  mutation CreateFile($input: CreateFileInput!) {
    createFile(input: $input) {
      id
      preSignedUploadUrl
      file {
        id
        filename
      }
    }
  }
`;

const FINALIZE_FILE_UPLOAD = gql`
  mutation FinalizeFileUpload($id: ID!) {
    finalizeFileUpload(id: $id) {
      id
      size
    }
  }
`;

async function uploadFile(client, refid, blob, filename, contentType) {
  // 1. Create the file record (size omitted).
  const { data: createData } = await client.mutate({
    mutation: CREATE_FILE,
    variables: {
      input: {
        refid,
        reftype: "item",
        filename,
        contentType,
      },
    },
  });

  const { id, preSignedUploadUrl } = createData.createFile;

  // 2. Upload bytes directly to S3.
  await fetch(preSignedUploadUrl, {
    method: "PUT",
    body: blob,
    headers: { "Content-Type": contentType },
  });

  // 3. Finalize: read the real size from S3 and persist it.
  const { data: finalizeData } = await client.mutate({
    mutation: FINALIZE_FILE_UPLOAD,
    variables: { id },
  });

  return finalizeData.finalizeFileUpload;
}
```

## Filtering Files by Finalization State

Use the new `sizeIsNil` and `sizeNotNil` predicates on `FileWhereInput` to identify files that are still pending finalization:

```graphql
# Find files awaiting finalizeFileUpload.
query PendingUploads {
  files(where: { sizeIsNil: true }) {
    edges {
      node {
        id
        filename
        createdAt
      }
    }
  }
}
```

```graphql
# Find fully uploaded files.
query CompletedUploads {
  files(where: { sizeNotNil: true }) {
    edges {
      node {
        id
        filename
        size
      }
    }
  }
}
```

## Important Notes

* Idempotent. Calling `finalizeFileUpload` multiple times for the same file is safe; subsequent calls return the existing record without emitting an `UPDATE` mutation event when the size already matches.
* The mutation issues an `S3 StatObject` call against the bucket. If the bytes have not been uploaded yet, the call fails and the `size` field remains unset.
* After a successful finalize, downstream consumers (search indexers, audit loggers, workflow triggers) receive the standard mutation event and can rely on `size` being populated.
* The mutation runs inside the same GraphQL transaction context as `createFile`, so it participates in pyck's tenant isolation and access policy enforcement.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.pyck.cloud/graphql-api/mutations/finalize-file-upload.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
