Skip to content

Commit

Permalink
feat: add endpoint for replace doc
Browse files Browse the repository at this point in the history
  • Loading branch information
ayocodingit committed Apr 22, 2024
1 parent 5fc5414 commit dae8bdf
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@master
with:
node-version: "14"
node-version: "18.17.0"

- name: Run Npm Install
run: npm install
Expand Down
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"body-parser": "^1.20.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"docxtemplater": "^3.47.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"helmet": "^6.0.1",
Expand All @@ -58,6 +59,7 @@
"mongoose": "^6.8.3",
"multer": "^1.4.5-lts.1",
"node-schedule": "^2.1.1",
"pizzip": "^3.1.7",
"puppeteer": "^19.6.3",
"redis": "^4.5.1",
"sharp": "^0.33.0",
Expand Down
13 changes: 13 additions & 0 deletions src/helpers/joi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Joi from 'joi'

export const uriWithSpaces = (
value: string,
helpers: Joi.CustomHelpers<string>
) => {
try {
new URL(value)
return value
} catch (error) {
return helpers.error('string.uri')
}
}
4 changes: 4 additions & 0 deletions src/helpers/regex.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export const RegexContentTypeImage = /^image\//
export const RegexContentTypeDoc =
/vnd.openxmlformats-officedocument.wordprocessingml.document/
export const RegexContentTypeAllow =
/vnd.openxmlformats-officedocument.wordprocessingml.document|image/
33 changes: 32 additions & 1 deletion src/modules/files/delivery/http/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
RequestConvertImage,
RequestImage,
RequestPdf,
RequestReplaceDoc,
RequestUpload,
} from '../../entity/schema'
import removeFile from '../../../../cron/removeFile.cron'
Expand Down Expand Up @@ -115,6 +116,36 @@ class Handler {
}
}
}
public ReplaceDoc() {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const body = ValidateFormRequest(RequestReplaceDoc, req.body)
const { filename, meta } = await this.usecase.ReplaceDoc(body)

this.logger.Info(statusCode[statusCode.OK], {
additional_info: this.http.AdditionalInfo(
req,
statusCode.OK
),
})

removeFile(this.minio, filename, body.seconds, this.logger)
const url = await this.minio.GetFileUrl(filename)
const shortlink = await this.shortlink.GenerateLink(
this.http.GetDomain(req) + `/download?url=${url}`,
body.seconds
)
return res.status(statusCode.OK).json({
data: {
url: shortlink,
...meta,
},
})
} catch (error) {
return next(error)
}
}
}

public Upload() {
return async (req: any, res: Response, next: NextFunction) => {
Expand Down Expand Up @@ -147,7 +178,7 @@ class Handler {
} catch (error) {
return next(error)
} finally {
rmSync(`${this.http.dir}/${req.file.filename}`)
if (req.file) rmSync(`${this.http.dir}/${req.file.filename}`)
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/modules/files/entity/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export interface RequestConvertImage {
seconds: number
}

export interface RequestReplaceDoc {
url: string
seconds: number
data: object
}

export interface RequestUpload {
seconds: number
file: {
Expand Down
13 changes: 10 additions & 3 deletions src/modules/files/entity/schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Joi from 'joi'
import config from '../../../config/config'
import { uriWithSpaces } from '../../../helpers/joi'

const seconds = 3600

export const RequestImage = Joi.object({
url: Joi.string().uri().required(),
url: Joi.string().custom(uriWithSpaces).required(),
property: Joi.object({
extension: Joi.string().valid('png', 'jpg').default('jpg'),
height: Joi.number().optional(),
Expand All @@ -15,7 +16,7 @@ export const RequestImage = Joi.object({
})

export const RequestPdf = Joi.object({
url: Joi.string().uri().required(),
url: Joi.string().custom(uriWithSpaces).required(),
property: Joi.object({
format: Joi.string().valid('a4', 'letter').default('a4'),
margin: Joi.object({
Expand All @@ -30,10 +31,16 @@ export const RequestPdf = Joi.object({
})

export const RequestConvertImage = Joi.object({
url: Joi.string().uri().required(),
url: Joi.string().custom(uriWithSpaces).required(),
seconds: Joi.number().default(seconds).optional(),
})

export const RequestReplaceDoc = Joi.object({
url: Joi.string().custom(uriWithSpaces).required(),
seconds: Joi.number().default(seconds).optional(),
data: Joi.object().optional(),
})

export const RequestUpload = Joi.object({
seconds: Joi.number().default(seconds).optional(),
file: Joi.object({
Expand Down
1 change: 1 addition & 0 deletions src/modules/files/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Files {
Router.post('/pdf', handler.Pdf())
Router.post('/convert-image', handler.ConvertImage())
Router.post('/upload', this.http.Upload('file'), handler.Upload())
Router.post('/replace-doc', handler.ReplaceDoc())

this.http.SetRouter('/v1/', Router)
}
Expand Down
47 changes: 46 additions & 1 deletion src/modules/files/usecase/usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
RequestConvertImage,
RequestImage,
RequestPdf,
RequestReplaceDoc,
RequestUpload,
} from '../entity/interface'
import fs, { readFileSync } from 'fs'
Expand All @@ -13,7 +14,11 @@ import axios from 'axios'
import error from '../../../pkg/error'
import statusCode from '../../../pkg/statusCode'
import Sharp from '../../../pkg/sharp'
import { RegexContentTypeImage } from '../../../helpers/regex'
import {
RegexContentTypeDoc,
RegexContentTypeImage,
} from '../../../helpers/regex'
import Docxtemplater from '../../../pkg/docxtemplater'

class Usecase {
constructor(
Expand Down Expand Up @@ -129,6 +134,46 @@ class Usecase {
}
}

public async ReplaceDoc({ url, data: payload }: RequestReplaceDoc) {
try {
const { data, status, headers } = await axios.get(url, {
responseType: 'arraybuffer',
})

const contentType = headers['content-type'] || ''

if (status === 200 && !RegexContentTypeDoc.test(contentType))
throw new error(
statusCode.BAD_REQUEST,
statusCode[statusCode.BAD_REQUEST]
)

const content = data.toString('binary')

const { filename } = this.getFiles('docx')

const source = Docxtemplater.ReplaceDoc(content, payload)

const size = Buffer.byteLength(source)

const mimetype =
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'

await this.minio.Upload(source, filename, size, mimetype)

return {
filename,
meta: {
filename,
size,
mimetype,
},
}
} catch (error: any) {
throw error
}
}

public async Upload(body: RequestUpload) {
const { source, meta } = await Sharp.ConvertToWebp(
readFileSync(body.file.path)
Expand Down
38 changes: 38 additions & 0 deletions src/pkg/docxtemplater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Docxtemplater from 'docxtemplater'
import PizZip from 'pizzip'
import error from './error'
import statusCode from './statusCode'

class ReplaceDoc {
constructor() {}

public static ReplaceDoc(content: string, data: object) {
try {
const zip = new PizZip(content)

// This will parse the template, and will throw an error if the template is
// invalid, for example, if the template is "{user" (no closing tag)
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
})

// Render the document (Replace {first_name} by John, {last_name} by Doe, ...)
doc.render(data)

// Get the zip document and generate it as a nodebuffer
const buf = doc.getZip().generate({
type: 'nodebuffer',
// compression: DEFLATE adds a compression step.
// For a 50MB output document, expect 500ms additional CPU time
compression: 'DEFLATE',
})

return buf
} catch (err: any) {
throw new error(statusCode.BAD_REQUEST, err.message)
}
}
}

export default ReplaceDoc
15 changes: 13 additions & 2 deletions src/transport/http/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import fs from 'fs'
import multer from 'multer'
import axios from 'axios'
import error from '../../pkg/error'
import { RegexContentTypeImage } from '../../helpers/regex'
import { RegexContentTypeAllow, RegexContentTypeDoc } from '../../helpers/regex'

class Http {
private app: Express
Expand Down Expand Up @@ -51,14 +51,25 @@ class Http {

if (
status === 200 &&
!RegexContentTypeImage.test(contentType)
!RegexContentTypeAllow.test(contentType)
)
throw new error(
statusCode.NOT_FOUND,
statusCode[statusCode.NOT_FOUND]
)

res.setHeader('Content-Type', contentType)

if (RegexContentTypeDoc.test(contentType)) {
url = url.split('?')[0]
const urls = url.split('/')
const filename = urls[urls.length - 1]
res.setHeader(
'Content-disposition',
`attachment; filename=${filename}`
)
}

return res.send(data)
} catch (error) {
return res.status(statusCode.NOT_FOUND).json({
Expand Down

0 comments on commit dae8bdf

Please sign in to comment.