File Uploads
File upload is the basic functionality for any application it is required to upload assets such as user avatar, files and other kind of documents. In PubNGo the file upload feature is required for
- User Avatars
- Government IDs and related legal documents
- Property images
there are several options available when it comes to decide how an application will upload files, where it will store them for later use and how fast it can retrieve them for later use. The retrieval of these files should be quick for better UX and overall application performance.
AWS S3
For file uploads we are using AWS S3. Amazon S3 or Amazon Simple Storage Service is a service offered by Amazon Web Services that provides object storage through a web service interface.
Bucket
To upload your data (photos, videos, documents, etc.) to Amazon S3, you must first create an S3 bucket in one of the AWS Regions. A bucket is a container for objects stored in Amazon S3. You can store any number of objects in a bucket and can have up to 100 buckets in your account. Read more
Pre signed URL
All objects and buckets by default are private. The presigned URLs are useful if you want your user/customer to be able to upload a specific object to your bucket, but you don't require them to have AWS security credentials or permissions. Read more
Enabling File Uploads
To enable Pubngo to upload files, you should start with writing the supporting mutations at the backend, for this documentation we'll follow the example of the Government ID.
Server Side
In backend, we need to write mutations and respective resolvers for those. See Government ID related mutations
In general, you need two type of mutations
- To generate Signed Upload Link
- To attach uploaded image to resource
the names of the mutations can whatever you want but the argument should match because all this information is required to fill up the underlying data model.
input UploadedFileInput{
name: String!
mimeType:String!
size: Float!
width: Float
height: Float
url:String!
}
extend type Mutation {
# generate Signed Upload Link
createFileUploadLinks(filenames:[String!]!): [String!]! @isAuthenticated @addAccountContext
# To attach uploaded image to resource
attachGovernmentID(idType: IDType, images: [UploadedFileInput!]!): GovernmentID @isAuthenticated @addAccountContext
uploadVerifyImage(image: UploadedFileInput!): String @isAuthenticated @addAccountContext
}
createFileUploadLinks(_, { filenames }, { fileInfoService, userContext }) {
return Promise.all(
filenames.map((filename) =>
fileInfoService.uploadByUrl({
filename,
// ID of the resource
ref: userContext.accountId,
// Type of resource
refType: IFileRefType.Account as never,
// ID of the user who requested the upload
userId: userContext.accountId,
}),
),
);
},
attachGovernmentID(_, { idType, images }, { extendedAccountService, userContext }) {
return extendedAccountService.attachGovernmentID(idType, images, userContext.accountId);
},
uploadVerifyImage(_, { image }, { extendedAccountService, userContext }) {
return extendedAccountService.uploadVerifyImage(image, userContext.accountId);
},
Client Side
From the client side all you need is to call these mutations in sequence and upload the image in between, following the above example the sequence will look something like the following
- Call
createFileUploadLinks
and you will receive an array of Signed URLS - Make an HTTP
put
request on the url with the actual file to upload the file directly to the AWS - Call
attachGovernmentID
and attach uploaded file with the meta information to the actual resource.
to avoid this repetitive effort we've already created a React hook which takes care of everything. For usage of the hook we can look at the example of Government ID use case.
const { startUpload: attachID, called: governmentIDUploaded } = useUploadImage<never, { idType: IIDType }>({
createUploadLink: {
name: 'createFileUploadLinks',
mutation: useCreateFileUploadLinksMutation,
processVariables: (files) => ({
filenames: Array.isArray(files) ? files.map(({ name }) => name) : [files.name],
})
},
saveUploadedFile: {
name: 'attachGovernmentID',
mutation: useAttachGovernmentIDMutation,
processVariables: (images, { idType }) => ({
idType,
images
})
}
})
const { startUpload: uploadFace, called: faceUploaded } = useUploadImage({
createUploadLink: {
name: 'createFileUploadLinks',
mutation: useCreateFileUploadLinksMutation,
processVariables: (files) => ({
filenames: Array.isArray(files) ? files.map(({ name }) => name) : [files.name],
})
},
saveUploadedFile: {
name: 'uploadVerifyImage',
mutation: useUploadVerifyImageMutation,
processVariables: (image) => ({
image: Array.isArray(image) ? image[0] : image,
})
}
})
The hook takes two arguments
- createUploadLink
- saveUploadedFile
both of them have same object structure
- name (Name of the mutation being called)
- mutation (Apollo Mutation Hook)
- processVariables
- This function will return the exact variables needed by the mutation.