Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 160 additions & 165 deletions code/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"prepare": "cd ../ && node code/.husky/install.mjs",
"publish:snapshot": "nx run-many -t publish:snapshot -- --verbose --tag next --registry $NPM_PUBLISHING_REGISTRY --unsafe-perm",
"start": "node --experimental-specifier-resolution=node --env-file=.env --loader ts-node/esm dist/server.js",
"start:dev": "cp .env dist && cd dist && NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=4096\" node --env-file=.env ./server.js",
"start:dev": "cp .env dist && cd dist && NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" node --env-file=.env ./server.js",
"storage:cleanup": "tsx --env-file=./src/actions/storage-cleanup/.env ./src/actions/storage-cleanup/action.ts",
"verify": "npm ci && npm run lint && npm run build",
"version:development": "export BUMP_DEVELOP_VERSION=$(npm version $(npm version minor)-SNAPSHOT) && lerna version $BUMP_DEVELOP_VERSION --no-git-tag-version --no-push --force-publish --exact --yes && npm version $(npm version minor)-SNAPSHOT",
Expand All @@ -59,9 +59,9 @@
"@azure/web-pubsub": "^1.2.0",
"@google/genai": "^1.32.0",
"@imgly/background-removal-node": "^1.4.5",
"@inditextech/weave-sdk": "2.17.0",
"@inditextech/weave-store-azure-web-pubsub": "2.17.0",
"@inditextech/weave-store-standalone": "2.17.0",
"@inditextech/weave-sdk": "2.18.0",
"@inditextech/weave-store-azure-web-pubsub": "2.18.0",
"@inditextech/weave-store-standalone": "2.18.0",
"@mastra/ai-sdk": "^0.3.1",
"@mastra/core": "^0.24.5",
"@mastra/libsql": "^0.16.2",
Expand Down
80 changes: 80 additions & 0 deletions code/src/api/v1/controllers/getExportedImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)
//
// SPDX-License-Identifier: Apache-2.0

import { Readable } from "stream";
import { Request, Response } from "express";
import { ImagesPersistenceHandler } from "../../../images/persistence.js";
import archiver from "archiver";

export const getExportedImageController = () => {
const persistenceHandler = new ImagesPersistenceHandler("exported-images");

return async (req: Request, res: Response): Promise<void> => {
const roomId = req.params.roomId as string;
const imageId = req.params.imageId as string;

const responseType: string = (req.query.responseType as string) ?? "blob";

const fileName = `${roomId}/${imageId}`;

if (!(await persistenceHandler.exists(fileName))) {
res
.status(404)
.json({ status: "KO", message: "Exported image doesn't exist" });
return;
}

const { response, mimeType } = await persistenceHandler.fetch(fileName);

if (response && response.readableStreamBody) {
const fileExtension = mimeType === "image/png" ? ".png" : ".jpg";

if (responseType === "blob") {
// Setting headers for the response
res.set("Cache-Control", "public, max-age=86400"); // 1 day
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader(
"Content-Disposition",
`attachment; filename="render${fileExtension}"`,
);
response.readableStreamBody.pipe(res);

return;
}

if (responseType === "zip") {
res.setHeader("Content-Type", "application/zip");
res.setHeader("Content-Disposition", "attachment; filename=export.zip");

const archive = archiver("zip", { zlib: { level: 9 } });
archive.pipe(res);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
archive.append(response.readableStreamBody as any, {
name: `render${fileExtension}`,
});

// Finalize ZIP
archive.finalize();
return;
}

const chunks: Uint8Array[] = [];

for await (const chunk of response.readableStreamBody as Readable) {
chunks.push(chunk);
}

const buffer = Buffer.concat(chunks);

res.status(200).json({
url: `data:${mimeType};base64,${buffer.toString("base64")}`,
});
} else {
res
.status(500)
.json({ status: "KO", message: "Error downloading image" });
}
};
};
78 changes: 78 additions & 0 deletions code/src/api/v1/controllers/getExportedPdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)
//
// SPDX-License-Identifier: Apache-2.0

import { Readable } from "stream";
import { Request, Response } from "express";
import { ImagesPersistenceHandler } from "../../../images/persistence.js";
import archiver from "archiver";

export const getExportedPdfController = () => {
const persistenceHandler = new ImagesPersistenceHandler("exported-pdf");

return async (req: Request, res: Response): Promise<void> => {
const roomId = req.params.roomId as string;
const pdfId = req.params.pdfId as string;

const responseType: string = (req.query.responseType as string) ?? "blob";

const fileName = `${roomId}/${pdfId}`;

if (!(await persistenceHandler.exists(fileName))) {
res
.status(404)
.json({ status: "KO", message: "Exported pdf doesn't exist" });
return;
}

const { response, mimeType } = await persistenceHandler.fetch(fileName);

if (response && response.readableStreamBody) {
const fileExtension = ".pdf";

if (responseType === "blob") {
// Setting headers for the response
res.set("Cache-Control", "public, max-age=86400"); // 1 day
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader(
"Content-Disposition",
`attachment; filename="render${fileExtension}"`,
);
response.readableStreamBody.pipe(res);

return;
}

if (responseType === "zip") {
res.setHeader("Content-Type", "application/zip");
res.setHeader("Content-Disposition", "attachment; filename=export.zip");

const archive = archiver("zip", { zlib: { level: 9 } });
archive.pipe(res);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
archive.append(response.readableStreamBody as any, {
name: `render${fileExtension}`,
});

// Finalize ZIP
archive.finalize();
return;
}

const chunks: Uint8Array[] = [];

for await (const chunk of response.readableStreamBody as Readable) {
chunks.push(chunk);
}

const buffer = Buffer.concat(chunks);

res.status(200).json({
url: `data:${mimeType};base64,${buffer.toString("base64")}`,
});
} else {
res.status(500).json({ status: "KO", message: "Error downloading pdf" });
}
};
};
65 changes: 65 additions & 0 deletions code/src/api/v1/controllers/postExportToImageAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)
//
// SPDX-License-Identifier: Apache-2.0

import { z } from "zod";
import { Request, Response } from "express";
import { WeaveExportFormats } from "@inditextech/weave-types";
import { ExportImageJob } from "@/workloads/jobs/export-image/job.js";
import { getJobHandler } from "@/workloads/workloads.js";
import { JOB_HANDLERS } from "@/workloads/constants.js";

const WeaveExportFormatsSchema: z.ZodType<WeaveExportFormats> = z.enum([
"image/png",
"image/jpeg",
]);

const payloadSchema = z.object({
roomData: z.string().base64(),
nodes: z.array(z.string()).optional().default([]),
options: z.object({
format: WeaveExportFormatsSchema.optional().default("image/png"),
backgroundColor: z.string().optional().default("transparent"),
padding: z.number().min(0).optional().default(20),
pixelRatio: z.number().min(1).optional().default(1),
quality: z.number().min(0).max(1).optional().default(1),
}),
responseType: z.enum(["base64", "blob", "zip"]).optional().default("blob"),
});

export const postExportToImageAsyncController = () => {
return async (req: Request, res: Response): Promise<void> => {
const roomId = req.params.roomId as string;

const userId: string = (req.headers["x-weave-user-id"] as string) ?? "";
const clientId: string = (req.headers["x-weave-client-id"] as string) ?? "";

const parsedBody = payloadSchema.safeParse(req.body);

if (!parsedBody.success) {
res.status(400).json({ errors: parsedBody.error.errors });
return;
}

const jobHandler = getJobHandler<ExportImageJob>(JOB_HANDLERS.EXPORT_IMAGE);

const id = await jobHandler.startExportImageJob(
clientId,
roomId,
userId,
parsedBody.data,
);

if (id) {
res.status(200).json({
status: "Export image job created OK",
jobId: id,
});
} else {
res.status(500).json({
status: "KO",
message: "Error creating export image job",
});
}
};
};
60 changes: 60 additions & 0 deletions code/src/api/v1/controllers/postExportToPDFAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)
//
// SPDX-License-Identifier: Apache-2.0

import { z } from "zod";
import { Request, Response } from "express";
import { getJobHandler } from "@/workloads/workloads.js";
import { JOB_HANDLERS } from "@/workloads/constants.js";
import { ExportPdfJob } from "@/workloads/jobs/export-pdf/job.js";

const payloadSchema = z.object({
roomData: z.string().base64(),
pages: z
.array(z.object({ title: z.string(), nodes: z.array(z.string()) }))
.optional()
.default([]),
options: z.object({
backgroundColor: z.string().optional().default("transparent"),
padding: z.number().min(0).optional().default(20),
pixelRatio: z.number().min(1).optional().default(1),
}),
responseType: z.enum(["base64", "blob", "zip"]).optional().default("blob"),
});

export const postExportToPDFAsyncController = () => {
return async (req: Request, res: Response): Promise<void> => {
const roomId = req.params.roomId as string;

const userId: string = (req.headers["x-weave-user-id"] as string) ?? "";
const clientId: string = (req.headers["x-weave-client-id"] as string) ?? "";

const parsedBody = payloadSchema.safeParse(req.body);

if (!parsedBody.success) {
res.status(400).json({ errors: parsedBody.error.errors });
return;
}

const jobHandler = getJobHandler<ExportPdfJob>(JOB_HANDLERS.EXPORT_PDF);

const id = await jobHandler.startExportPdfJob(
clientId,
roomId,
userId,
parsedBody.data,
);

if (id) {
res.status(200).json({
status: "Export pdf job created OK",
jobId: id,
});
} else {
res.status(500).json({
status: "KO",
message: "Error creating export pdf job",
});
}
};
};
8 changes: 4 additions & 4 deletions code/src/api/v1/controllers/workers/exportToImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ parentPort?.on("message", async ({ config, roomData, nodes, options }) => {
pixelRatio: options.pixelRatio,
backgroundColor: options.backgroundColor,
quality: options.quality,
}
},
);

destroy();
Expand Down Expand Up @@ -48,7 +48,7 @@ parentPort?.on("message", async ({ config, roomData, nodes, options }) => {

const buffer = imageBuffer.buffer.slice(
imageBuffer.byteOffset,
imageBuffer.byteOffset + imageBuffer.byteLength
imageBuffer.byteOffset + imageBuffer.byteLength,
);

parentPort?.postMessage(buffer, [buffer as ArrayBuffer]);
Expand All @@ -60,11 +60,11 @@ parentPort?.on("message", async ({ config, roomData, nodes, options }) => {
message: (error as Error).message,
},
}),
"utf8"
"utf8",
);
const ab = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
buffer.byteOffset + buffer.byteLength,
);
parentPort?.postMessage(ab, [ab as ArrayBuffer]);
}
Expand Down
Loading
Loading