Support TS, swagger,Template(xls,docx),PDF convert

This commit is contained in:
Sorawit Bholsithi 2023-10-07 17:29:53 +07:00
parent b221deb1cf
commit 8d7654423a
84 changed files with 3604 additions and 1 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
dist2
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
# docker build -t docker.frappet.com/demo/report-server .
# docker push docker.frappet.com/demo/report-server
# docker run --name rserver -p 80:3000 docker.frappet.com/demo/report-server
FROM node:20
# ENV PANDOC_VERSION 3.1.7
ENV TZ=Asia/Bangkok
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN mkdir -p /usr/share/fonts/truetype/th
COPY ./ThaiFonts/*.ttf /usr/share/fonts/truetype/th/
# RUN fc-cache -f -v
RUN apt-get -qq update && apt-get -qq -y install wget fonts-noto fonts-noto-cjk libreoffice --no-install-recommends
RUN fc-cache -f && rm -rf /var/cache/* && apt -y autoremove && rm -rf /var/lib/apt/lists/* && apt-get clean
RUN mkdir /app
WORKDIR /app
COPY templates templates
COPY package.json .
ENV NODE_ENV production
RUN npm install
COPY dist2 .
EXPOSE 80
CMD ["node","app.js"]

View file

@ -1,2 +1,57 @@
# report-server-ts
Typescript and bun port of Report Server (Javascript node)
เป็น Web API ออกแบบมาเพื่อสร้างเอกสาร สามารถใช้ frontend โดยตรง เพื่อจะได้ไม่ต้องเขียนโปรแกรมทำเอกสารเฉพาะแต่ละตัวออกมา ปรับปรุงของเดิมจาก
เขียนใหม่เป็น TypeScript ตัด pandoc ออก เริ่มแรกลองใช้ bun แทน node รองรับ TypeScript โดยไม่ต้องตั้งค่า แต่มีปัญหากับ libreoffice-file-converter ต้องแก้ค่าใน package.json และ docker-template คาดว่าเป็นปัญหาจาก eval ทำให้ ส่วน EXEC กับ custom function ทำงานไม่ได้ เลยกลับมาใช้ node ตั้งค่าของ TypeScript [ตามเวปนี้](https://www.geeksforgeeks.org/how-to-use-express-in-typescript/) ให้ใช้ ES module ได้ด้วย
```bash
npm i express
npm i -D typescript @types/express @types/node ts-node
npm i docx-templates xlsx-template-next swagger-ui-express swagger-jsdoc yaqrcode libreoffice-file-converter
npm i -D @types/swagger-ui-express @types/swagger-jsdoc
# obfuscate code tools
npm i -D javascript-obfuscator
# add type support for yaqrcode
cd node_modules/yaqrcode
wget https://raw.githubusercontent.com/zenozeng/node-yaqrcode/master/index.d.ts
```
# การใช้งาน
ดู scripts ใน package.json และ compose.yaml
```bash
npm run dev
npm run build
npm run serve
npm run obfuscator
npm run preview
npm run build:docker
docker compose up -d
```
# ทดสอบ template
ไปที่โฟลเดอร์ test-run จะมีโค้ดเพื่อทดสอบ template docx และ xlsx อย่างง่าย ใช้ค่า default ได้เลย สามารถทดสอบการแปลงไปไฟล์แบบต่างๆที่ Libreoffice รองรับได้ ควรทดสอบรูปแบบข้อมูล(json) ให้เข้ากับ template(docx,xlsx) ก่อนใช้งานเพราะ error log จะแสดงที่เซิร์เวอร์เท่านั้น
``` bash
$ npx ts-node docx-template.ts
Output extension(docx,pdf,odt): pdf
JSON data path(./docx.json):
Base path of templates-docx(..):
$ npx ts-node xlsx-template.ts
Output extension(xlsx,pdf): ods
JSON data path(./xlsx.json):
Base path of templates-docx(..):
```
# Build docker
```bash
docker build -t docker.frappet.com/demo/report-server .
docker push docker.frappet.com/demo/report-server
docker run --name rserver -p 80:3000 docker.frappet.com/demo/report-server
```
## report-templates/docx
ใช้ library [docx-templates](https://www.npmjs.com/package/docx-templates) รับข้อมูลใน json กับ MS Word (template) เพิ่อสร้างเอกสาร MS Word สำหรับการใช้งาน PDF จะใช้ LibreOffice ในการแปลงจาก docx เป็น pdf เพื่อจะได้ตัดคำภาษาไทยได้
## report-templates/xlsx
comming soon

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Baijam.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Charmonman.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Fahkwang.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH K2D July8.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH KoHo Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH KoHo.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Kodchasal.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Krub Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Krub.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Niramit AS.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/TH Srisakdi.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/THSarabun.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ThaiFonts/THSarabunNew.ttf Normal file

Binary file not shown.

27
app.ts Normal file
View file

@ -0,0 +1,27 @@
/*
* Report Server
* Web API Path
* Node.js reverse proxy pandoc swan
* demo frontent public
*/
import swaggerspecs from "./libs/swagger-specs.json"
import swaggerUi from "swagger-ui-express"
import express, { Express, Request, Response } from 'express'
import { docxTemplateRoute } from './libs/docx-templates-lib'
import { xlsxTemplateRoute } from './libs/xlsx-template-lib'
import { convertTemplateRoute } from './libs/convert-libs'
const app: Express = express()
const port: number = Number(process.env.PORT) || 80;
app.use(express.json()); //application/json
app.use(express.raw()); //application/octet-stream
app.use(express.urlencoded({ extended: true }));
app.use("/swagger", swaggerUi.serve, swaggerUi.setup(swaggerspecs,{ explorer: true }));
app.get('/', (req: Request, res: Response) => {
res.json({
message: 'Hello report-template API !!',
})
})
app.use('/api/v1/report-template/docx', docxTemplateRoute);
app.use('/api/v1/report-template/xlsx', xlsxTemplateRoute);
app.use('/api/v1/report-template/convert', convertTemplateRoute);
app.listen(port, () => console.log(`Application is running on port ${port}`))

8
compose.yaml Normal file
View file

@ -0,0 +1,8 @@
version: "3.2"
services:
report-server:
image: docker.frappet.com/demo/report-server:latest
ports:
- 3000:80
volumes:
- ./templates:/app/templates

94
libs/convert-libs.ts Normal file
View file

@ -0,0 +1,94 @@
import express from 'express'
export const convertTemplateRoute = express.Router();
import {mimeToExtension} from './report-template'
import { LibreOfficeFileConverter } from 'libreoffice-file-converter';
/** javascript-obfuscator:disable
* @swagger
* tags:
* name: office-convert
* description: ใช้แปลงไฟล์จากเอกสารที่ libreoffice x
*/
/** javascript-obfuscator:disable
* @swagger
* /api/v1/report-template/convert:
* post:
* summary: Create document from template
* tags: [office-convert]
* parameters:
* - name: report_name
* in: header
* description: นามสกุลไฟล์ที่อัปโหลดเข้ามา( soffice )
* required: true
* schema:
* type: string
* example: report
* requestBody:
* required: true
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* responses:
* 201:
* description: file was converted.
* content:
* application/pdf:
* schema:
* type: string
* format: binary
* application/vnd.oasis.opendocument.text:
* schema:
* type: string
* format: binary
* application/vnd.openxmlformats-officedocument.wordprocessingml.document:
* schema:
* type: string
* format: binary
* application/msword:
* schema:
* type: string
* format: binary
* text/html:
* schema:
* type: string
* 500:
* description: Server error
*
*/
convertTemplateRoute.post("/convert", async function (req, res) {
try {
if (!req.headers['content-type'] ||
!req.headers['accept'] ||
!req.headers['report_name'] ||
req.headers['content-type'] !== "application/octet-stream") {
res.statusCode = 400;
res.statusMessage = 'Require header: content-type(application/octet-stream) accept, report_name';
res.end(res.statusMessage);
console.log(req.headers['content-type'],req.headers['accept'],req.headers['report_name'])
return
}
let outputMediaType = mimeToExtension(req.headers['accept']);
let reportName = req.headers['report_name']
console.log('output: ' + outputMediaType);
const libreOfficeFileConverter = new LibreOfficeFileConverter({
childProcessOptions: {
timeout: 60 * 1000,
},
});
const buffer = await libreOfficeFileConverter.convertBuffer(req.body, outputMediaType);
res.statusCode = 201;
res.setHeader('Content-Type', req.headers['accept']);
res.setHeader('Content-Disposition', `attachment;filename=${reportName}.${outputMediaType}`);
res.setHeader('Content-Length', buffer.length);
res.end(buffer);
} catch (ex) {
res.statusCode = 500;
res.statusMessage = 'Internal Server Error';
res.end(res.statusMessage);
console.error(`Error during convert with soffice:`, ex);
}
})

View file

@ -0,0 +1,36 @@
// https://swagger.io/docs/specification/about/
import swaggerJsdoc from "swagger-jsdoc"
import fs from 'fs'
const swaggerOptions = {
definition: {
openapi: "3.1.0",
info: {
title: "Report Server",
version: "0.1.0",
description:
"Advance Create and convert document API for microservice.",
license: {
name: "by Frappet",
url: "https://frappet.com",
},
},
servers: [
{
url: "https://report-server.frappet.synology.me",
},
{
url: "http://localhost:3000",
},
{
url: "http://192.168.2.100:3000",
},
],
},
apis: ["./libs/*.ts"],
};
export function createSpec(){
const swaggerSpecs = swaggerJsdoc(swaggerOptions);
fs.promises.writeFile("libs/swagger-specs.json",JSON.stringify(swaggerSpecs,null,2))
}
createSpec()

147
libs/docx-templates-lib.ts Normal file
View file

@ -0,0 +1,147 @@
import express from 'express'
export const docxTemplateRoute = express.Router();
import {mimeToExtension,templateData} from './report-template'
import fs from 'fs'
import { createReport } from 'docx-templates';
import qrcode from 'yaqrcode'
// แก้ package.json ของ LibreOfficeFileConverter
// https://github.com/microsoft/TypeScript/issues/52363#issuecomment-1659179354
import { LibreOfficeFileConverter } from 'libreoffice-file-converter';
const TEMPLATE_FOLDER_NAME = "templates/docx"
/**
* docxTemplate Uses docx-template to convert input data and template to output buffer.
* You have to handle exception throw by function
* template keep in folder templates
* @param {String} base base path of caller relate to template-docx foler (no trail slash)
* @param {templateData} tdata Template Information in JSON format
* @param {String} outputMediaType output extension
* @return {Promise<Uint8Array>} output buffer after apply template.
*/
export async function docxTemplateX(base: string=".", tdata: templateData, outputMediaType: string="docx"): Promise<Uint8Array> {
try {
let template = await fs.promises.readFile(`${base}/${TEMPLATE_FOLDER_NAME}/${tdata.template}.docx`)
const buffer = await createReport({
template,
data: tdata.data,
additionalJsContext: {
qrCode: (contents: string, caption: string, size: number, width: number, height: number) => {
const dataUrl = qrcode(contents, { size: size });
const data = dataUrl.slice('data:image/gif;base64,'.length);
return { width, height, data, extension: '.gif', caption };
},
}
});
if (outputMediaType === "docx")
return buffer
const libreOfficeFileConverter = new LibreOfficeFileConverter({
childProcessOptions: {
timeout: 60 * 1000,
},
});
const lbuffer = await libreOfficeFileConverter.convertBuffer(Buffer.from(buffer), outputMediaType);
return lbuffer
} catch (e) {
throw (e)
}
}
/** javascript-obfuscator:disable
* @swagger
* /api/v1/report-template/docx:
* get:
* summary: list docx template
* tags: [report-template]
* responses:
* 200:
* description: array of template
* content:
* applicatin/json:
* schema:
* type: array
* items:
* type: string
* example: ["hello"]
* 500:
* description: Server error
*/
docxTemplateRoute.get("/", async function (req, res) {
try {
const fileList = await fs.promises.readdir(`./${TEMPLATE_FOLDER_NAME}`)
const templateList = fileList.map((f) => f.split('.docx')[0])
res.send(templateList)
} catch (ex) {
res.statusCode = 500;
res.statusMessage = 'Internal Server Error';
res.end(res.statusMessage);
console.error('Error during get template list: ', ex);
}
})
/** javascript-obfuscator:disable
* @swagger
* /api/v1/report-template/docx:
* post:
* summary: Create document from docx template output can be docx pdf odt
* tags: [report-template]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/templateData'
* example:
* template: hello
* reportName: docx-report
* data: {"docNo":"๑๒๓๔๕","me": "กระผม","prefix": "นาย","name": "สรวิชญ์","surname": "พลสิทธิ์","position": "Chief Technology Officer","org": {"type": "บริษัท","name": "เฟรปเป้ที","url": "https://frappet.com"},"employees": [{"name": "ภาวิชญ์","surname": "พลสิทธิ์"},{"name": "วิชญาภา","surname": "พลสิทธิ์"}]}
* responses:
* 201:
* description: เอกสารถูกสร้างขึ้น
* content:
* application/vnd.openxmlformats-officedocument.wordprocessingml.document:
* schema:
* type: string
* format: binary
* application/pdf:
* schema:
* type: string
* format: binary
* application/vnd.oasis.opendocument.text:
* schema:
* type: string
* format: binary
* 500:
* description: Server error
*
*/
docxTemplateRoute.post("/", async function (req, res) {
try {
if (!req.headers['content-type'] || !req.headers['accept'])
throw new Error("Require header content-type, accept")
let inputType = mimeToExtension(req.headers['content-type']);
let outputMediaType = mimeToExtension(req.headers['accept']);
console.log('content-type: ', inputType);
console.log('accept: ', outputMediaType);
let buffer = await docxTemplateX('.', req.body, outputMediaType)
res.statusCode = 201;
res.setHeader('Content-Type', req.headers['accept']);
res.setHeader('Content-Disposition', `attachment;filename=${req.body.reportName}.${outputMediaType}`);
res.setHeader('Content-Length', buffer.length);
res.end(buffer);
} catch (ex) {
res.statusCode = 500;
res.statusMessage = 'Internal Server Error during get docx template list';
res.end(res.statusMessage);
console.error('Error during apply template: ', ex);
}
})

67
libs/report-template.ts Normal file
View file

@ -0,0 +1,67 @@
export interface templateData {
template: string;
reportName: string;
data: object
}
export interface IDictionary<TValue> {
[key: string]: TValue;
}
export function mimeToExtension(mime: string): string {
const mimeList: IDictionary<string> = {
"application/pdf": "pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/msword": "doc",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
"application/vnd.ms-excel": "xls",
"application/vnd.ms-powerpoint": "ppt",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
"application/vnd.oasis.opendocument.text": "odt",
"application/vnd.oasis.opendocument.spreadsheet":"ods",
"application/vnd.oasis.opendocument.presentation":"odp",
"text/html": "html",
"application/json": "json",
"text/csv": "csv",
"text/markdown": "md",
"text/plain": "txt",
"application/rtf": "rtf"
}
if (mimeList[mime]) {
return mimeList[mime]
} else if (mime.match(/^application\/x\./)) {
return mime.substr(mime.indexOf('.') + 1);
} else {
return mime
}
}
/** javascript-obfuscator:disable
* @swagger
* tags:
* name: report-template
* description: API Template docx xlsx Mail Merge Template soffice
*/
/** javascript-obfuscator:disable
* @swagger
* components:
* schemas:
* templateData:
* type: object
* required:
* - template
* - reportName
* - data
* - finished
* properties:
* template:
* type: string
* description: name of template
* reportName:
* type: string
* description: name of report for download
* data:
* type: object
* description: value for template
*/

348
libs/swagger-specs.json Normal file
View file

@ -0,0 +1,348 @@
{
"openapi": "3.1.0",
"info": {
"title": "Report Server",
"version": "0.1.0",
"description": "Advance Create and convert document API for microservice.",
"license": {
"name": "by Frappet",
"url": "https://frappet.com"
}
},
"servers": [
{
"url": "https://report-server.frappet.synology.me"
},
{
"url": "http://localhost:3000"
},
{
"url": "http://192.168.2.100:3000"
}
],
"paths": {
"/api/v1/report-template/convert": {
"post": {
"summary": "Create document from template",
"tags": [
"office-convert"
],
"parameters": [
{
"name": "report_name",
"in": "header",
"description": "นามสกุลไฟล์ที่อัปโหลดเข้ามา(ตอนนี้ไม่จำเป็นต้องใช้เพราะ soffice ตรวจสอบเอง)",
"required": true,
"schema": {
"type": "string",
"example": "report"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"responses": {
"201": {
"description": "file was converted.",
"content": {
"application/pdf": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/vnd.oasis.opendocument.text": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/msword": {
"schema": {
"type": "string",
"format": "binary"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"500": {
"description": "Server error"
}
}
}
},
"/api/v1/report-template/docx": {
"get": {
"summary": "list docx template",
"tags": [
"report-template"
],
"responses": {
"200": {
"description": "array of template",
"content": {
"applicatin/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"example": [
"hello"
]
}
}
},
"500": {
"description": "Server error"
}
}
},
"post": {
"summary": "Create document from docx template output can be docx pdf odt",
"tags": [
"report-template"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/templateData"
},
"example": {
"template": "hello",
"reportName": "docx-report",
"data": {
"docNo": "๑๒๓๔๕",
"me": "กระผม",
"prefix": "นาย",
"name": "สรวิชญ์",
"surname": "พลสิทธิ์",
"position": "Chief Technology Officer",
"org": {
"type": "บริษัท",
"name": "เฟรปเป้ที",
"url": "https://frappet.com"
},
"employees": [
{
"name": "ภาวิชญ์",
"surname": "พลสิทธิ์"
},
{
"name": "วิชญาภา",
"surname": "พลสิทธิ์"
}
]
}
}
}
}
},
"responses": {
"201": {
"description": "เอกสารถูกสร้างขึ้น",
"content": {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/pdf": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/vnd.oasis.opendocument.text": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "Server error"
}
}
}
},
"/api/v1/report-template/xlsx": {
"get": {
"summary": "แสดงรายการ xlsx template",
"tags": [
"report-template"
],
"responses": {
"200": {
"description": "array of template",
"content": {
"applicatin/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"example": [
"hello"
]
}
}
},
"500": {
"description": "Server error"
}
}
},
"post": {
"summary": "สร้าองเอกสารจาก xlsx template แล้วส่งกลับมาเป็น xlsx pdf odt",
"tags": [
"report-template"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/templateData"
},
"example": {
"template": "hello",
"reportName": "xlsx-report",
"data": {
"docNo": "๑๒๓๔๕",
"me": "กระผม",
"prefix": "นาย",
"name": "สรวิชญ์",
"surname": "พลสิทธิ์",
"position": "Chief Technology Officer",
"org": {
"type": "บริษัท",
"name": "เฟรปเป้ที",
"url": "https://frappet.com"
},
"employees": [
{
"id": 1,
"name": "ภาวิชญ์",
"surname": "พลสิทธิ์",
"score": 80
},
{
"id": 2,
"name": "วิชญาภา",
"surname": "พลสิทธิ์",
"score": 50
},
{
"id": 3,
"name": "ฐิตาภา",
"surname": "พลสิทธิ์",
"score": 90
},
{
"id": 4,
"name": "สรวิชญ์ พลสิทธิ์",
"surname": "พลสิทธิ์",
"score": 99
}
]
}
}
}
}
},
"responses": {
"201": {
"description": "เอกสารถูกสร้างขึ้น created.",
"content": {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/pdf": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/vnd.oasis.opendocument.text": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "Server error"
}
}
}
}
},
"components": {
"schemas": {
"templateData": {
"type": "object",
"required": [
"template",
"reportName",
"data",
"finished"
],
"properties": {
"template": {
"type": "string",
"description": "name of template"
},
"reportName": {
"type": "string",
"description": "name of report for download"
},
"data": {
"type": "object",
"description": "value for template"
}
}
}
}
},
"tags": [
{
"name": "office-convert",
"description": "ใช้แปลงไฟล์จากเอกสารที่ libreoffice แปลงได้ x"
},
{
"name": "report-template",
"description": "API สำหรับสร้างเอกสารจาก Template docx หรือ xlsx การทำงานคล้ายการทำ Mail Merge ทำให้ยูสเซอร์ทั่วไปแก้ Template ได้ง่าย สามารถแปลงไฟล์นามสกุลอื่นๆที่ soffice รองรับ"
}
]
}

134
libs/xlsx-template-lib.ts Normal file
View file

@ -0,0 +1,134 @@
import express from 'express'
export const xlsxTemplateRoute = express.Router();
import {mimeToExtension,templateData} from './report-template'
import { ExcelTemplate } from 'xlsx-template-next'
import fs from 'fs'
import { LibreOfficeFileConverter } from 'libreoffice-file-converter';
const TEMPLATE_FOLDER_NAME = "templates/xlsx"
/**
* docxTemplate Uses docx-template to convert input data and template to output buffer.
* You have to handle exception throw by function
* template keep in folder templates
* @param {String} base base path of caller relate to template-docx foler (no trail slash)
* @param {templateData} tdata Template Information in JSON format
* @param {String} outputMediaType output extension
* @param {Number} tab tab page of spread sheet , default = 1
* @return {Promise<Uint8Array>} output buffer after apply template.
*/
export async function xlsxTemplateX(base: string=".", tdata: templateData, outputMediaType: string="xlsx",tab:number=1): Promise<Uint8Array> {
try {
const template = new ExcelTemplate();
let templateBuff = await fs.promises.readFile(`${base}/${TEMPLATE_FOLDER_NAME}/${tdata.template}.xlsx`)
await template.load(templateBuff);
await template.process(tab,tdata.data)
const buffer = await template.build({ type: 'uint8array' })
if (outputMediaType === "xlsx")
return buffer as Uint8Array
const libreOfficeFileConverter = new LibreOfficeFileConverter({
childProcessOptions: {
timeout: 60 * 1000,
},
});
const lbuffer = await libreOfficeFileConverter.convertBuffer(Buffer.from(buffer as Uint8Array), outputMediaType);
return lbuffer
} catch (e) {
throw (e)
}
}
/** javascript-obfuscator:disable
* @swagger
* /api/v1/report-template/xlsx:
* get:
* summary: แสดงรายการ xlsx template
* tags: [report-template]
* responses:
* 200:
* description: array of template
* content:
* applicatin/json:
* schema:
* type: array
* items:
* type: string
* example: ["hello"]
* 500:
* description: Server error
*/
xlsxTemplateRoute.get("/", async function (req, res) {
try {
const fileList = await fs.promises.readdir(`./${TEMPLATE_FOLDER_NAME}`)
const templateList = fileList.map((f) => f.split('.xlsx')[0])
res.send(templateList)
} catch (ex) {
res.statusCode = 500;
res.statusMessage = 'Internal Server Error during get xlsx template list';
res.end(res.statusMessage);
console.error('Error during get template list: ', ex);
}
})
/** javascript-obfuscator:disable
* @swagger
* /api/v1/report-template/xlsx:
* post:
* summary: สร้าองเอกสารจาก xlsx template xlsx pdf odt
* tags: [report-template]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/templateData'
* example:
* template: hello
* reportName: xlsx-report
* data: {"docNo": "๑๒๓๔๕","me": "กระผม","prefix": "นาย","name": "สรวิชญ์","surname": "พลสิทธิ์","position": "Chief Technology Officer","org": {"type": "บริษัท","name": "เฟรปเป้ที","url": "https://frappet.com"},"employees": [{"id":1,"name": "ภาวิชญ์","surname": "พลสิทธิ์","score":80},{"id":2,"name": "วิชญาภา","surname": "พลสิทธิ์","score":50},{"id":3,"name": "ฐิตาภา","surname": "พลสิทธิ์","score":90},{"id":4,"name": "สรวิชญ์ พลสิทธิ์","surname": "พลสิทธิ์","score":99}]}
* responses:
* 201:
* description: เอกสารถูกสร้างขึ้น created.
* content:
* application/vnd.openxmlformats-officedocument.wordprocessingml.document:
* schema:
* type: string
* format: binary
* application/pdf:
* schema:
* type: string
* format: binary
* application/vnd.oasis.opendocument.text:
* schema:
* type: string
* format: binary
* 500:
* description: Server error
*
*/
xlsxTemplateRoute.post("/", async function (req, res) {
try {
if (!req.headers['content-type'] || !req.headers['accept'])
throw new Error("Require header content-type, accept")
let inputType = mimeToExtension(req.headers['content-type']);
let outputMediaType = mimeToExtension(req.headers['accept']);
console.log('content-type: ', inputType);
console.log('accept: ', outputMediaType);
let buffer = await xlsxTemplateX(".",req.body,outputMediaType)
res.statusCode = 201;
res.setHeader('Content-Type', req.headers['accept']);
res.setHeader('Content-Disposition', `attachment;filename=${req.body.reportName}.${outputMediaType}`);
res.setHeader('Content-Length', buffer.length);
res.end(buffer);
} catch (ex) {
res.statusCode = 500;
res.statusMessage = 'Internal Server Error';
res.end(res.statusMessage);
console.error('Error during apply template: ', ex);
}
})

2500
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "report-server-ts",
"version": "1.0.0",
"description": "docx-template มีปัญหากับ bun คาดว่าเป็นปัญหาจาก eval",
"scripts": {
"dev": "PORT=3000 nodemon app.ts",
"swaggergen":"ts-node libs/create-swagger-spec.ts ",
"build": "ts-node libs/create-swagger-spec.ts && tsc && cp libs/swagger-specs.json dist/libs",
"serve": "PORT=3000 node dist/app.js",
"obfuscator": "tsc && javascript-obfuscator ./dist --output ./dist2 && cp libs/swagger-specs.json dist/libs",
"preview": "PORT=3000 node dist2/app.js",
"build:docker": "ts-node libs/create-swagger-spec.ts && tsc && javascript-obfuscator ./dist --output ./dist2&& cp libs/swagger-specs.json dist2/libs && docker build -t docker.frappet.com/demo/report-server ."
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"docx-templates": "^4.11.3",
"express": "^4.18.2",
"libreoffice-file-converter": "^1.2.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"xlsx-template-next": "^0.1.2",
"yaqrcode": "^0.2.1"
},
"devDependencies": {
"@types/express": "^4.17.18",
"@types/node": "^20.8.2",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.4",
"javascript-obfuscator": "^4.1.0",
"nodemon": "^3.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}

BIN
templates/docx/blank.docx Normal file

Binary file not shown.

BIN
templates/docx/hello.docx Normal file

Binary file not shown.

BIN
templates/xlsx/hello.xlsx Normal file

Binary file not shown.

21
test-run/docx-template.ts Normal file
View file

@ -0,0 +1,21 @@
// npx ts-node docx-templates.ts
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
import { stdin as input, stdout as output } from 'node:process';
import { docxTemplateX} from '../src/libs/docx-templates-lib';
import {templateData} from '../src/libs/report-template'
import fs from 'fs';
(async ()=>{
const rl = readline.createInterface({ input, output });
const e = await rl.question('Output extension(docx,pdf,odt): ');
const ext =e?e:"docx"
const dpath = await rl.question('JSON data path(./docx.json): ');
const datapath = dpath?dpath:"./docx.json"
const data_raw = fs.readFileSync(datapath);
const tdata:templateData = JSON.parse(data_raw.toString());
const bpath = await rl.question('Base path of templates-docx(..): ');
const basepath = bpath?bpath:".."
let buffer = await docxTemplateX(basepath,tdata,ext)
fs.writeFileSync(tdata.reportName+"."+ext, buffer);
rl.close();
})()

28
test-run/docx.json Normal file
View file

@ -0,0 +1,28 @@
{
"template": "hello",
"reportName": "report-docx",
"data": {
"docNo": "๑๒๓๔๕",
"me": "กระผม",
"prefix": "นาย",
"name": "สรวิชญ์",
"surname": "พลสิทธิ์",
"position": "Chief Technology Officer",
"org": {
"type": "บริษัท",
"name": "เฟรปเป้ที",
"url": "https://frappet.com"
},
"employees": [
{
"name": "ภาวิชญ์",
"surname": "พลสิทธิ์"
},
{
"name": "วิชญาภา",
"surname": "พลสิทธิ์"
}
]
}
}

BIN
test-run/report-docx.pdf Normal file

Binary file not shown.

BIN
test-run/report-xlsx.pdf Normal file

Binary file not shown.

BIN
test-run/report.docx Normal file

Binary file not shown.

BIN
test-run/report.odt Normal file

Binary file not shown.

BIN
test-run/report.pdf Normal file

Binary file not shown.

21
test-run/xlsx-template.ts Normal file
View file

@ -0,0 +1,21 @@
// npx ts-node xlsx-template.ts
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
import { stdin as input, stdout as output } from 'node:process';
import {xlsxTemplateX} from '../src/libs/xlsx-template-lib'
import {templateData} from '../src/libs/report-template'
import fs from 'fs';
(async ()=>{
const rl = readline.createInterface({ input, output });
const e = await rl.question('Output extension(xlsx,pdf,ods): ');
const ext =e?e:"xlsx"
const dpath = await rl.question('JSON data path(./xlsx.json): ');
const datapath = dpath?dpath:"./xlsx.json"
const data_raw = fs.readFileSync(datapath);
const tdata:templateData = JSON.parse(data_raw.toString());
const bpath = await rl.question('Base path of templates-docx(..): ');
const basepath = bpath?bpath:".."
let buffer = await xlsxTemplateX(basepath,tdata,ext)
fs.writeFileSync(tdata.reportName+"."+ext, buffer);
rl.close();
})()

44
test-run/xlsx.json Normal file
View file

@ -0,0 +1,44 @@
{
"template": "hello",
"reportName": "report-xlsx",
"data": {
"docNo": "๑๒๓๔๕",
"me": "กระผม",
"prefix": "นาย",
"name": "สรวิชญ์",
"surname": "พลสิทธิ์",
"position": "Chief Technology Officer",
"org": {
"type": "บริษัท",
"name": "เฟรปเป้ที",
"url": "https://frappet.com"
},
"employees": [
{
"id":1,
"name": "ภาวิชญ์",
"surname": "พลสิทธิ์",
"score":80
},
{
"id":2,
"name": "วิชญาภา",
"surname": "พลสิทธิ์",
"score":50
},
{
"id":3,
"name": "ฐิตาภา",
"surname": "พลสิทธิ์",
"score":90
},
{
"id":4,
"name": "สรวิชญ์ พลสิทธิ์",
"surname": "พลสิทธิ์",
"score":99
}
]
}
}

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"rootDir": "./",
"outDir": "dist",
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true
},
"include": ["libs/**/*.ts","app.ts"],
"exclude": ["node_modules","test-run"]
}