@canva/cli
Version:
The official Canva CLI.
235 lines (208 loc) • 7.41 kB
text/typescript
/**
* DISCLAIMER:
* This file contains a demonstration of how to implement a simple API with Express.js for educational purposes only.
* It is NOT SUITABLE for use in a production environment. It lacks many essential features such as error handling,
* input validation, authentication, and security measures. Additionally, the in-memory job queue is for illustrative
* purposes only and is not efficient or scalable. For production use, consider using robust libraries and frameworks,
* implementing proper error handling, security measures, and using appropriate database and job queue solutions.
* Use this code as a learning resource, but do not deploy it in a real backend without significant modifications
* to ensure reliability, security, and scalability.
*/
import * as express from "express";
interface ImageResponse {
fullsize: { width: number; height: number; url: string };
thumbnail: { width: number; height: number; url: string };
}
// Array of placeholder image URLs.
// In a real-world scenario, these URLs would point to dynamically generated images.
const imageUrls: ImageResponse[] = [
{
fullsize: {
width: 1280,
height: 853,
url: "https://cdn.pixabay.com/photo/2023/02/03/05/11/youtube-background-7764170_1280.jpg",
},
thumbnail: {
width: 640,
height: 427,
url: "https://cdn.pixabay.com/photo/2023/02/03/05/11/youtube-background-7764170_640.jpg",
},
},
{
fullsize: {
width: 1280,
height: 853,
url: "https://cdn.pixabay.com/photo/2023/02/03/05/12/youtube-background-7764172_1280.jpg",
},
thumbnail: {
width: 640,
height: 427,
url: "https://cdn.pixabay.com/photo/2023/02/03/05/12/youtube-background-7764172_640.jpg",
},
},
{
fullsize: {
width: 1280,
height: 853,
url: "https://cdn.pixabay.com/photo/2023/02/03/05/07/colorful-7764162_1280.jpg",
},
thumbnail: {
width: 640,
height: 427,
url: "https://cdn.pixabay.com/photo/2023/02/03/05/07/colorful-7764162_640.jpg",
},
},
{
fullsize: {
width: 1280,
height: 853,
url: "https://cdn.pixabay.com/photo/2023/02/03/04/57/swirls-7764142_1280.jpg",
},
thumbnail: {
width: 640,
height: 427,
url: "https://cdn.pixabay.com/photo/2023/02/03/04/57/swirls-7764142_640.jpg",
},
},
];
export const createImageRouter = () => {
const enum Routes {
CREDITS = "/api/credits",
PURCHASE_CREDITS = "/api/purchase-credits",
QUEUE_IMAGE_GENERATION = "/api/queue-image-generation",
JOB_STATUS = "/api/job-status",
CANCEL_JOB = "/api/job-status/cancel",
}
const router = express.Router();
const jobQueue: {
jobId: string;
prompt: string;
timeoutId: NodeJS.Timeout;
}[] = [];
const completedJobs: Record<string, ImageResponse[]> = {};
const cancelledJobs: { jobId: string }[] = [];
// Initial credit allocation for users, which decreases with each use.
// Users receive 10 credits initially and can purchase additional credits in bundles.
let credits = 10;
const CREDITS_IN_BUNDLE = 10;
/**
* GET endpoint to retrieve user credits.
* Returns the current number of credits available to the user.
*/
router.get(Routes.CREDITS, async (req, res) => {
res.status(200).send({
credits,
});
});
/**
* POST endpoint to purchase credits.
* Increments the user's credits by the number of credits in a bundle.
* This endpoint should be backed by proper input validation to prevent misuse.
*/
router.post(Routes.PURCHASE_CREDITS, async (req, res) => {
credits += CREDITS_IN_BUNDLE;
res.status(200).send({
credits,
});
});
/**
* GET endpoint to generate images based on a prompt.
* Generates images based on the provided prompt and adds a job to the processing queue.
* If there are not enough credits, it returns a 403 error.
* If the prompt parameter is missing, it returns a 400 error.
* Once the job is added to the queue, it returns a jobId that can be used to check the job status.
* Note: The job processing time is simulated to be 5 seconds.
*/
router.get(Routes.QUEUE_IMAGE_GENERATION, async (req, res) => {
if (credits <= 0) {
return res
.status(403)
.send("Not enough credits required to generate images.");
}
const prompt = req.query.prompt as string;
if (!prompt) {
return res.status(400).send("Missing prompt parameter.");
}
const jobId = generateJobId();
const timeoutId = setTimeout(() => {
const index = jobQueue.findIndex((job) => job.jobId === jobId);
if (index !== -1) {
jobQueue.splice(index, 1);
completedJobs[jobId] = imageUrls.map((image) => {
return { ...image, label: prompt };
});
// Reduce credits by 1 when images are successfully generated
credits -= 1;
}
}, 5000); // Simulating 5 seconds processing time
// Add the job to the jobQueue along with the timeoutId
jobQueue.push({ jobId, prompt, timeoutId });
res.status(200).send({
jobId,
});
});
/**
* GET endpoint to check the status of a job.
* Retrieves the status of a job identified by its jobId parameter.
* If the job is completed, it returns the images generated by the job.
* If the job is still in the processing queue, it returns "processing".
* If the job has been cancelled, it returns "cancelled".
* If the job is not found, it returns a 404 error.
*/
router.get(Routes.JOB_STATUS, async (req, res) => {
const jobId = req.query.jobId as string;
if (!jobId) {
return res.status(400).send("Missing jobId parameter.");
}
if (completedJobs[jobId]) {
return res.status(200).send({
status: "completed",
images: completedJobs[jobId],
credits,
});
}
if (jobQueue.some((job) => job.jobId === jobId)) {
return res.status(200).send({
status: "processing",
});
}
if (cancelledJobs.some((job) => job.jobId === jobId)) {
return res.status(200).send({
status: "cancelled",
});
}
return res.status(404).send("Job not found.");
});
/**
* POST endpoint to cancel a job.
* Cancels a job identified by its jobId parameter.
* If the job is found and successfully cancelled, it removes the job from the processing queue and adds it to the cancelled jobs array.
* If the job is not found, it returns a 404 error.
*/
router.post(Routes.CANCEL_JOB, async (req, res) => {
const jobId = req.query.jobId as string;
if (!jobId) {
return res.status(400).send("Missing jobId parameter.");
}
const index = jobQueue.findIndex((job) => job.jobId === jobId);
if (index !== -1) {
cancelledJobs.push({ jobId });
// If the job is found, remove it from the jobQueue
const { timeoutId } = jobQueue[index];
jobQueue.splice(index, 1);
// Also clear the timeout associated with this job if it exists
if (timeoutId) {
clearTimeout(timeoutId);
}
return res.status(200).send("Job successfully cancelled.");
}
return res.status(404).send("Job not found.");
});
/**
* Generates a unique job ID.
*/
function generateJobId(): string {
return Math.random().toString(36).substring(2, 15);
}
return router;
};