Uploading & Downloading
The SDKs wrap the whole upload flow in one call: client.files.upload on the
server, usePyloUpload in the browser. In the browser the bytes stream straight to Pylo, so you
need no extra API route and no file bytes cross your server. The hook reports byte-accurate progress
and error state, and it can abort a batch.
Uploading
1
2
3
4
5
6
7
8
9
10
11
12
import { pylo } from './lib/pylo';
import { readFile } from 'node:fs/promises';
const buffer = await readFile('./avatar.png');
const media = await pylo.files.upload(buffer, {
fileName: 'avatar.png',
mimeType: 'image/png',
entityRelationPath: 'contact.avatar',
});
// media: { id, fileName, mimeType, size }Accepted sources on the server are File, Blob, ArrayBuffer, and typed arrays (including Node
Buffer). fileName is required unless the source is a File. Passing entityRelationPath
enforces the relation's mime and extension rules at upload time; omit it for unrestricted,
unattached uploads. isPublic: true creates a permanent public download URL.
Attaching to a record
attachTo connects the uploaded file in the same call. Use mode: 'add' for list relations. On the
client it also invalidates the entity's queries, so open views refetch.
1
2
3
4
5
6
7
await pylo.files.upload(buffer, {
fileName: 'avatar.png',
entityRelationPath: 'contact.avatar',
attachTo: { id: contactId },
});
// list relations: attachTo: { id: projectId, mode: 'add' }Upload now, attach later
If you do not know the target record yet, upload without attachTo. The result always contains the
PyloMedia id; connect it later with a normal upsert:
1
2
3
4
5
6
7
8
9
10
const media = await pylo.files.upload(buffer, {
fileName: 'report.pdf',
entityRelationPath: 'project.documents',
});
// later, once the record exists:
await pylo.project.upsert({
id: projectId,
documents_add: [{ id: media.id }],
});Downloading
Private download URLs expire (about 5 minutes by default), so fetch them right before use and never store them. Public files return their permanent URL from the same calls.
1
2
const url = await pylo.files.getDownloadUrl(mediaId);
const response = await fetch(url);usePyloFileUrl caches the URL and refetches it before the expiry. You can also select url on the
relation in a regular query; each fetch returns fresh URLs.
Upload URLs
createUploadUrl returns the upload URL without sending bytes, for example to hand it to another
process or a language without an SDK. It expires after about 1 hour. POST the file to url as
multipart/form-data with a single file part.
1
2
3
4
const uploadUrl = await pylo.files.createUploadUrl({
entityRelationPath: 'project.documents',
});
// uploadUrl: { id, url }usePyloUpload reference
usePyloUpload(options?) returns:
startUpload(files, overrides?): uploads one file or an array, resolves with the created PyloMedia rows ({ id, fileName, mimeType, size }each)isUploading: whether a batch is in flightprogress:0-100, aggregated by bytes across the batchuploadedFiles: results of the last successful batcherror: the failure of the last batch, ornullabort(): cancels the in-flight batchreset(): clears progress, error, and results
Options (all optional; you can also pass entityRelationPath, isPublic, and attachTo per call,
as the second argument of startUpload):
entityRelationPath: the media relation the files are destined for, for example'contact.avatar'(autocompletes from your schema)isPublic: create permanent public download URLs (requiresentityRelationPath)attachTo:{ id, mode? }, connects each uploaded file to the record;modeis'set'(default, single relations) or'add'(list relations)onProgress,onSuccess,onError: callbacksheaders: extra headers for the GraphQL requests
Progress reporting
In the browser, progress is byte-accurate. In Node, onProgress fires only at the start and the
end of the transfer, because the runtime's fetch emits no upload progress events.