lib/common/xml-data-bridge/xml-data-bridge-export.service.ts
XmlDataBridgeExportService is responsible for exporting data through XML.
Methods |
|
constructor(logger: Logger, mediaService: MediaManager, filesService: FileManager, cacheService: CacheService, explorerService: ExplorerService)
|
||||||||||||||||||
Parameters :
|
Private buildXmlBody | ||||||||
buildXmlBody(decomposedEntityStack: XdbDecomposedEntity[])
|
||||||||
Builds the XML body from the decomposed entity stack.
Parameters :
Returns :
any
The XML body as a string. |
Private Async decomposeEntity | ||||||||||||||||
decomposeEntity(object: ObjectLiteral, target: TargetData, tParams: ExplorerTargetParams)
|
||||||||||||||||
Decomposes an entity into a stack of decomposed entities.
Parameters :
Returns :
unknown
A promise that resolves to the stack of decomposed entities. |
Async exportXml | ||||||||
exportXml(params: XdbExportParams)
|
||||||||
Inherited from
XdbExportService
|
||||||||
Defined in
XdbExportService:84
|
||||||||
Export XML data to a specified target.
Parameters :
Returns :
Promise<XdbExportDto>
A string if object exported to zip-file |
Private getKeyPropertyName | ||||||||||||
getKeyPropertyName(target: TargetData, node: ObjectLiteral)
|
||||||||||||
Gets the key property name for an entity.
Parameters :
Returns :
any
The key property name. |
Private Async handleDecomposedFiles | ||||||||||||
handleDecomposedFiles(stack: XdbDecomposedEntity[], operationDir: string)
|
||||||||||||
Handles decomposed file entities.
Parameters :
Returns :
unknown
A promise that resolves to an array of handled file paths. |
Private Async handleDecomposedMedias | ||||||||||||
handleDecomposedMedias(stack: XdbDecomposedEntity[], operationDir: string)
|
||||||||||||
Handles decomposed media entities.
Parameters :
Returns :
unknown
A promise that resolves to an array of handled media file paths. |
Private handleNode | ||||||||||||||||||||||||
handleNode(stack: XdbDecomposedEntity[], node: ObjectLiteral, fieldName: string, path?: string, arrayClassName?: string)
|
||||||||||||||||||||||||
Handles a node in the stack of decomposed entities.
Parameters :
Returns :
void
|
Private Async handleStackRoots | ||||||||||||
handleStackRoots(stack: XdbDecomposedEntity[], tParams: ExplorerTargetParams)
|
||||||||||||
Handles stack roots.
Parameters :
Returns :
unknown
A promise that resolves to the handled stack. |
Private handleStringValues | ||||||||
handleStringValues(stack: XdbDecomposedEntity[])
|
||||||||
Handles string values in the stack of decomposed entities.
Parameters :
Returns :
void
|
Private hasNestedNodes | ||||||||
hasNestedNodes(node: ObjectLiteral)
|
||||||||
Checks if a node has nested nodes.
Parameters :
Returns :
boolean
A boolean indicating whether the node has nested nodes. |
Private isPrimitiveNode | ||||||||
isPrimitiveNode(node: ObjectLiteral)
|
||||||||
Checks if a node is primitive.
Parameters :
Returns :
boolean
A boolean indicating whether the node is primitive. |
Private Async markReferences | ||||||||||||||||||||||||||||
markReferences(rootPrimaryCol: ExplorerColumn, tParams: ExplorerTargetParams, stack: XdbDecomposedEntity[], node: ObjectLiteral, parent: ObjectLiteral, path: string)
|
||||||||||||||||||||||||||||
Marks references in the stack of decomposed entities.
Parameters :
Returns :
any
|
import { Inject, Injectable, Logger } from "@nestjs/common";
import { ObjectLiteral } from "typeorm";
import * as path from "path";
import * as fs from "fs";
import * as AdmZip from "adm-zip";
import { XmlDataBridgeFileSchema } from "./xml-data-bridge-file-schema";
import { FilesUtils } from "../../shared/utils/files.utils";
import { NumberUtils } from "../../shared/utils/number.utils";
import { Xdb, XdbExportService } from "./xml-data-bridge.constants";
import { ReservedMediaFormat } from "../media/media.constants";
import { LOGGER } from "../../shared/modules/log/log.constants";
import { Media, MediaFormat, MediaManager } from "../media/media.types";
import { FileManager } from "../file/file.constants";
import { CacheService } from "../../shared/modules/cache/cache.types";
import {
ExplorerColumn,
ExplorerService,
ExplorerTargetParams,
TargetData,
} from "../explorer/explorer.types";
import {
XdbDecomposedEntity,
XdbExportDto,
XdbExportParams,
} from "./xml-data-bridge.types";
import { KpConfig } from "../../../gen-src/kp.config";
import { MediaEntity } from "../media/entity/media.entity";
import { FileEntity } from "../file/entity/file.entity";
import { MediaFileEntity } from "../media/entity/media-file.entity";
import { MediaFormatEntity } from "../media/entity/media-format.entity";
import { File } from "../file/file.types";
import { FileConfig } from "../../../gen-src/file.config";
import { MediaConfig } from "../../../gen-src/media.config";
import xmlFileSchemaTpl = XmlDataBridgeFileSchema.xmlFileSchemaTpl;
import xmlFileInsertUpdateNodeTpl = XmlDataBridgeFileSchema.xmlFileInsertUpdateNodeTpl;
import BODY_TOKEN = XmlDataBridgeFileSchema.BODY_TOKEN;
import createDirectoriesIfNotExist = FilesUtils.createDirectoriesIfNotExist;
import generateRandomInt = NumberUtils.generateRandomInt;
import xmlFileRowNode = XmlDataBridgeFileSchema.xmlFileRowNode;
import xmlFileRowPropertyNode = XmlDataBridgeFileSchema.xmlFileRowPropertyNode;
import rootToken = Xdb.rootToken;
import THUMB = ReservedMediaFormat.THUMB;
import ORIGINAL = ReservedMediaFormat.ORIGINAL;
import xmlMediaNodeTpl = XmlDataBridgeFileSchema.xmlMediaNodeTpl;
import readFile = FilesUtils.readFile;
import xmlFileNodeTpl = XmlDataBridgeFileSchema.xmlFileNodeTpl;
/**
* XmlDataBridgeExportService is responsible for exporting data through XML.
*/
@Injectable()
export class XmlDataBridgeExportService extends XdbExportService {
constructor(
@Inject(LOGGER) private readonly logger: Logger,
private readonly mediaService: MediaManager,
private readonly filesService: FileManager,
private readonly cacheService: CacheService,
private readonly explorerService: ExplorerService,
) {
super();
}
/**
* Export XML data to a specified target.
* @param params - object with export params XdbExportParams
* @returns A string if object exported to zip-file
*/
async exportXml(params: XdbExportParams): Promise<XdbExportDto> {
const tParams: ExplorerTargetParams = {
readRequest: true,
checkUserAccess: params.user,
};
const target = await this.explorerService.getTargetData(
params.target,
tParams,
);
const entity = await this.explorerService.getEntityData(
params.target,
params.id,
params.depth,
tParams,
);
if (params.excludeProperties?.length) {
for (const property of params.excludeProperties) {
delete entity[property];
}
}
const decomposedEntityStack = await this.decomposeEntity(
entity,
target,
tParams,
);
const tmpDir =
process.cwd() + (await this.cacheService.get(KpConfig.TMP_DIR));
const fileName = `${params.target.toLowerCase()}-${params.id}-${generateRandomInt()}`;
const operationDir = `${tmpDir}/${fileName}`;
await createDirectoriesIfNotExist(operationDir);
const xmlFilePath = `${operationDir}/${fileName}.xml`;
const handledMedias = await this.handleDecomposedMedias(
decomposedEntityStack,
operationDir,
);
const handledFiles = await this.handleDecomposedFiles(
decomposedEntityStack,
operationDir,
);
this.handleStringValues(decomposedEntityStack);
const xmlBody = this.buildXmlBody(decomposedEntityStack);
await fs.promises.writeFile(xmlFilePath, xmlBody);
if (!params.useFiles) {
return { file: `${fileName}/${fileName}.xml` };
}
const filesForZip = [xmlFilePath, ...handledFiles, ...handledMedias];
const zipFilePath = `${operationDir}/${fileName}.zip`;
const zip = new AdmZip();
for (const filePatch of filesForZip) {
zip.addLocalFile(filePatch);
}
zip.writeZip(zipFilePath);
return { file: `${fileName}/${fileName}.zip` };
}
/**
* Builds the XML body from the decomposed entity stack.
* @param decomposedEntityStack - The stack of decomposed entities.
* @returns The XML body as a string.
*/
private buildXmlBody(decomposedEntityStack: XdbDecomposedEntity[]) {
let body = ``;
for (const entity of decomposedEntityStack) {
if (entity.metadata.type === MediaEntity.name) {
const node = xmlMediaNodeTpl(entity.data);
body += node;
continue;
}
if (entity.metadata.type === FileEntity.name) {
const node = xmlFileNodeTpl(entity.data);
body += node;
continue;
}
const node = xmlFileInsertUpdateNodeTpl(entity.metadata.type);
let rowBody = ``;
for (const key of Object.keys(entity.data)) {
const row = xmlFileRowPropertyNode(
decomposedEntityStack,
key,
entity.data[key],
);
if (!row) {
continue;
}
rowBody += row;
}
body += node.replace(
BODY_TOKEN,
xmlFileRowNode().replace(BODY_TOKEN, rowBody),
);
}
const xmlBody = xmlFileSchemaTpl();
return xmlBody.replace(BODY_TOKEN, body);
}
/**
* Decomposes an entity into a stack of decomposed entities.
* @param object - The entity object.
* @param target - The target data.
* @param tParams - The target parameters.
* @returns A promise that resolves to the stack of decomposed entities.
*/
private async decomposeEntity(
object: ObjectLiteral,
target: TargetData,
tParams: ExplorerTargetParams,
) {
let stack: XdbDecomposedEntity[] = [];
this.handleNode(stack, object, rootToken);
// remove root duplicates
const rootNode = stack.find((v) => v.metadata.fieldName === rootToken);
const rootPrimaryCol = target.entity.columns.find((v) => v.primary);
for (let i = stack.length - 1; i >= 0; i--) {
const node = stack[i]; // handle medias
if (node.metadata.type === MediaFileEntity.name) {
stack.splice(i, 1);
continue;
}
if (node.metadata.type === MediaFormatEntity.name) {
const format = node.data as MediaFormat;
if (format.code === THUMB || format.code === ORIGINAL) {
stack.splice(i, 1);
continue;
}
}
if (node.metadata.type === MediaEntity.name) {
const media = node.data as MediaEntity;
if (!media.files?.length) {
stack.splice(i, 1);
} else {
delete media.files;
}
continue;
}
if (
node.metadata.fieldName === rootToken ||
node.metadata.type !== target.entity.target
) {
continue;
}
if (
node.data[rootPrimaryCol.property] ===
rootNode.data[rootPrimaryCol.property]
) {
stack.splice(i, 1);
}
}
stack = Xdb.removeDuplicateObjects(stack).reverse();
const root = stack[stack.length - 1];
await this.markReferences(
rootPrimaryCol,
tParams,
stack,
root.data,
root.data,
rootToken,
);
// mark root object
for (const key of Object.keys(root.data)) {
if (this.isPrimitiveNode(root.data[key])) {
continue;
}
if (Array.isArray(root.data[key])) {
for (let i = 0; i < root.data[key].length; i++) {
const target = await this.explorerService.getTargetData(
root.data[key][i].constructor.name,
tParams,
);
const p = `${rootToken}/${key}`;
const keyName = this.getKeyPropertyName(target, root.data[key][i]);
root.data[key][i] = `${p}#${keyName}:${root.data[key][i][keyName]}`;
}
} else {
const target = await this.explorerService.getTargetData(
root.data[key].constructor.name,
tParams,
);
const p = `${rootToken}/${key}`;
const keyName = this.getKeyPropertyName(target, root.data[key]);
root.data[key] = `${p}#${keyName}:${root.data[key][keyName]}`;
}
}
// clean keys
this.getKeyPropertyName(target, root.data);
return await this.handleStackRoots(stack, tParams);
}
/**
* Handles stack roots.
* @param stack - The stack of decomposed entities.
* @param tParams - The target parameters.
* @returns A promise that resolves to the handled stack.
*/
private async handleStackRoots(
stack: XdbDecomposedEntity[],
tParams: ExplorerTargetParams,
) {
for (let i = stack.length - 1; i >= 0; i--) {
for (const key of Object.keys(stack[i].data)) {
if (this.isPrimitiveNode(stack[i].data[key])) {
continue;
}
if (Array.isArray(stack[i].data[key])) {
if (!stack[i].data[key].length) {
delete stack[i].data[key];
continue;
}
if (
typeof stack[i].data[key][0] === "string" &&
stack[i].data[key][0].startsWith(rootToken)
) {
continue;
}
for (const j in stack[i].data[key]) {
if (!stack[i].data[key][j]) {
continue;
}
const target = await this.explorerService.getTargetData(
stack[i].data[key][j].constructor.name,
tParams,
);
const keyName = this.getKeyPropertyName(
target,
stack[i].data[key][j],
);
const stackEl = stack.find(
(v) =>
v.metadata.type === target.entity.target &&
v.data[keyName] === stack[i].data[key][j][keyName],
);
stack[i].data[key][j] =
`${stackEl.metadata.path}#${keyName}:${stack[i].data[key][j][keyName]}`;
}
} else {
if (stack[i].data[key].constructor.name === "Object") {
stack[i].data[key] = JSON.stringify(stack[i].data[key]);
continue;
}
const target = await this.explorerService.getTargetData(
stack[i].data[key].constructor.name,
tParams,
);
const keyName = this.getKeyPropertyName(target, stack[i].data[key]);
const stackEl = stack.find(
(v) =>
v.metadata.type === target.entity.target &&
v.data[keyName] === stack[i].data[key][keyName],
);
stack[i].data[key] =
`${stackEl.metadata.path}#${keyName}:${stack[i].data[key][keyName]}`;
}
}
}
return stack;
}
/**
* Marks references in the stack of decomposed entities.
* @param rootPrimaryCol - The root primary column.
* @param tParams - The target parameters.
* @param stack - The stack of decomposed entities.
* @param node - The current node.
* @param parent - The parent node.
* @param path - The current path.
*/
private async markReferences(
rootPrimaryCol: ExplorerColumn,
tParams: ExplorerTargetParams,
stack: XdbDecomposedEntity[],
node: ObjectLiteral,
parent: ObjectLiteral,
path: string,
) {
const root = stack[stack.length - 1];
for (const key in node) {
if (this.isPrimitiveNode(node[key])) {
continue;
}
if (Array.isArray(node[key])) {
if (!node[key].length) {
delete node[key];
continue;
}
for (const arrEl of node[key]) {
if (!arrEl) {
continue;
}
const target = await this.explorerService.getTargetData(
arrEl.constructor.name,
tParams,
);
const p = `${path}/${key}`;
const stackEl = stack.find(
(v) =>
v.metadata.path === p &&
v.data[target.primaryColumn.property] ===
arrEl[target.primaryColumn.property],
);
if (!stackEl) {
continue;
}
for (const sDataElKey of Object.keys(stackEl.data)) {
if (this.isPrimitiveNode(stackEl.data[sDataElKey])) {
continue;
}
if (stackEl.data[sDataElKey].constructor.name === "Object") {
stackEl.data[sDataElKey] = JSON.stringify(
stackEl.data[sDataElKey],
);
continue;
}
const sdTarget = await this.explorerService.getTargetData(
stackEl.data[sDataElKey].constructor.name,
tParams,
);
if (!sdTarget) {
continue;
}
const sdP = `${p}/${sDataElKey}`;
if (
sdTarget.entity.target === root.metadata.type &&
stackEl.data[sDataElKey][rootPrimaryCol.property] ===
root.data[rootPrimaryCol.property]
) {
delete stackEl.data[sDataElKey];
continue;
}
const keyName = this.getKeyPropertyName(
sdTarget,
stackEl.data[sDataElKey],
);
stackEl.data[sDataElKey] =
`${sdP}#${keyName}:${stackEl.data[sDataElKey][keyName]}`;
}
}
} else {
if (this.hasNestedNodes(node[key])) {
await this.markReferences(
rootPrimaryCol,
tParams,
stack,
node[key],
node,
`${path}/${key}`,
);
} else {
if (node[key].constructor.name === "Object") {
node[key] = JSON.stringify(node[key]);
continue;
}
const target = await this.explorerService.getTargetData(
node[key].constructor.name,
tParams,
);
const p = `${path}/${key}`;
const keyName = this.getKeyPropertyName(target, node[key]);
node[key] = `${p}#${keyName}:${node[key][keyName]}`;
await this.markReferences(
rootPrimaryCol,
tParams,
stack,
node[key],
node,
`${path}/${key}`,
);
}
}
}
}
/**
* Handles a node in the stack of decomposed entities.
* @param stack - The stack of decomposed entities.
* @param node - The current node.
* @param fieldName - The field name.
* @param path - The current path.
* @param arrayClassName - The class name for array elements.
*/
private handleNode(
stack: XdbDecomposedEntity[],
node: ObjectLiteral,
fieldName: string,
path?: string,
arrayClassName?: string,
) {
if (this.isPrimitiveNode(node)) {
return;
}
if (node.constructor.name === "Object") {
return;
}
const newPath = path ? `${path}/${fieldName}` : fieldName;
if (!Array.isArray(node) || (Array.isArray(node) && node?.length > 0)) {
const type = arrayClassName ?? node.constructor.name;
if (type !== "Array") {
stack.push({
metadata: { type, fieldName, path: newPath },
data: node,
});
}
}
if (!Array.isArray(node)) {
for (const key of Object.keys(node)) {
if (node[key] === null) {
continue;
}
this.handleNode(stack, node[key], key, newPath);
}
} else {
const arrNode = node as Array<ObjectLiteral>;
const arrClassName = arrNode[0]?.constructor?.name;
for (const e of arrNode) {
this.handleNode(stack, e, fieldName, path, arrClassName);
}
}
}
/**
* Checks if a node is primitive.
* @param node - The node to check.
* @returns A boolean indicating whether the node is primitive.
*/
private isPrimitiveNode(node: ObjectLiteral) {
return !node || typeof node !== "object" || node instanceof Date;
}
/**
* Checks if a node has nested nodes.
* @param node - The node to check.
* @returns A boolean indicating whether the node has nested nodes.
*/
private hasNestedNodes(node: ObjectLiteral) {
for (const key in node) {
if (!this.isPrimitiveNode(node[key])) {
return true;
}
}
return false;
}
/**
* Gets the key property name for an entity.
* @param target - The target data.
* @param node - The entity object.
* @returns The key property name.
*/
private getKeyPropertyName(target: TargetData, node: ObjectLiteral) {
const primaryProperty = target.primaryColumn.property;
const uniqColumns = target.entity.columns.filter((v) => v.unique);
if (!uniqColumns?.length) {
return primaryProperty;
}
for (const col of uniqColumns) {
if (
node[col.property] &&
(typeof node[col.property] === "string" ||
typeof node[col.property] === "number")
) {
if (col.property !== primaryProperty) {
if (
target.entity.target !== MediaEntity.name &&
target.entity.target !== FileEntity.name
) {
delete node[primaryProperty];
}
}
return col.property;
}
}
const propertyName = uniqColumns[0].property;
node[propertyName] =
`${target.entity.target.toLowerCase()}_${node[primaryProperty]}_${generateRandomInt(99999)}`;
if (
target.entity.target !== MediaEntity.name &&
target.entity.target !== FileEntity.name
) {
delete node[primaryProperty];
}
return propertyName;
}
/**
* Handles decomposed media entities.
* @param stack - The stack of decomposed entities.
* @param operationDir - The operation directory.
* @returns A promise that resolves to an array of handled media file paths.
*/
private async handleDecomposedMedias(
stack: XdbDecomposedEntity[],
operationDir: string,
) {
const result: string[] = [];
for (const item of stack) {
if (item.metadata.type !== MediaEntity.name) {
continue;
}
const node = item.data as Media & { file: string };
const media = await this.mediaService.findMediaById(node.id);
const file = media.files.find((v) => v.format.code);
const cfgProp = media.type.private
? MediaConfig.PRIVATE_DIR
: MediaConfig.PUBLIC_DIR;
const loc = await this.cacheService.get(cfgProp);
const fileName = `${file.name}.${media.type.ext.code}`;
const tmpFileName = `${generateRandomInt()}.${media.type.ext.code}`;
const mediaFilePath = path.normalize(
process.cwd() + `${loc}/${media.id}/${fileName}`,
);
const tmpFilePath = path.normalize(`${operationDir}/${tmpFileName}`);
const fileData = await readFile(mediaFilePath);
await fs.promises.writeFile(tmpFilePath, fileData);
node.file = tmpFileName;
result.push(tmpFilePath);
}
return result;
}
/**
* Handles decomposed file entities.
* @param stack - The stack of decomposed entities.
* @param operationDir - The operation directory.
* @returns A promise that resolves to an array of handled file paths.
*/
private async handleDecomposedFiles(
stack: XdbDecomposedEntity[],
operationDir: string,
) {
const result: string[] = [];
for (const item of stack) {
if (item.metadata.type !== FileEntity.name) {
continue;
}
const node = item.data as File & { file: string };
const fileEntity = await this.filesService.findFileById(node.id);
const cfgProp = !fileEntity.public
? FileConfig.PRIVATE_DIR
: FileConfig.PUBLIC_DIR;
const loc = await this.cacheService.get(cfgProp);
const fileName = `${fileEntity.path}`;
const tmpFileName = `${generateRandomInt()}.${fileName.split(".").pop()}`;
const filePath = path.normalize(
process.cwd() + `${loc}/${fileEntity.id}/${fileName}`,
);
const tmpFilePath = path.normalize(`${operationDir}/${tmpFileName}`);
const fileData = await readFile(filePath);
await fs.promises.writeFile(tmpFilePath, fileData);
node.file = tmpFileName;
result.push(tmpFilePath);
}
return result;
}
/**
* Handles string values in the stack of decomposed entities.
* @param stack - The stack of decomposed entities.
*/
private handleStringValues(stack: XdbDecomposedEntity[]) {
for (const item of stack) {
for (const key of Object.keys(item.data)) {
if (
typeof item.data[key] === "string" &&
/<[^>]+>/g.test(item.data[key])
) {
item.data[key] = `<![CDATA[${item.data[key]}]]>`;
}
}
}
}
}