@enonic/mock-xp
Version:
Mock Enonic XP API JavaScript Library
525 lines (459 loc) • 13.5 kB
text/typescript
import type {
Node,
NodePropertiesOnCreate,
} from '@enonic-types/lib-node';
import type {
// ExportNodesError,
// ExportNodesParams,
// ExportNodesResult,
// ImportNodesError,
ImportNodesParams,
ImportNodesResult,
// ResourceKey,
// exportNodes,
// importNodes
} from '@enonic-types/lib-export';
import type { Server } from '../implementation/Server';
import { isStringLiteral } from '@enonic/js-utils/value/isStringLiteral';
import AdmZip from 'adm-zip';
import { readdirSync, readFileSync, statSync } from 'fs';
import { homedir } from 'os';
import {
basename,
dirname,
// extname,
join,
normalize,
// relative,
resolve,
sep,
} from 'path';
import { sortZipEntries } from './export/sortZipEntries';
import { parseEnonicXml } from './export/parseEnonicXml';
import { UUID_NIL } from '../constants';
interface Entry {
absPath: string;
isDirectory: boolean;
name: string;
nodeParentPath: string;
}
const EXPORT_UUID_NIL = '000-000-000-000';
function isDirectory(path: string) {
try {
return statSync(path).isDirectory();
} catch (err) {
return false; // Path doesn't exist or other error
}
}
function isFile(path: string) {
try {
return statSync(path).isFile();
} catch (err) {
return false; // Path doesn't exist or other error
}
}
function listDirSync(
dirPath: string,
nodeParentPath: string,
// relativeFrom?: string,
): Entry[] {
return readdirSync(dirPath, {
// recursive: true,
// encoding: 'utf-8',
withFileTypes: true
}).map(entry => {
// console.log(entry);
const absPath = join(dirPath, entry.name);
return {
absPath,
isDirectory: entry.isDirectory(),
name: entry.name,
nodeParentPath,
// relative: relative(relativeFrom || dirPath, absPath),
}
}).sort(({name}, {name: nameB}) => name.localeCompare(nameB));
}
export class LibExport {
readonly sandboxAbsPath: string;
readonly server: Server;
private _importRootNode({
_debug,
_trace,
importNodesResult,
includePermissions,
rootXmlString,
}: {
_debug: boolean;
_trace: boolean;
importNodesResult: ImportNodesResult;
includePermissions: boolean;
rootXmlString: string;
}) {
const rootXmlNode = parseEnonicXml({
// _debug,
_trace,
log: this.server.log,
xmlString: rootXmlString,
});
if (_trace) this.server.log.debug('rootXmlNode:%s', rootXmlNode);
if (rootXmlNode._id !== EXPORT_UUID_NIL) {
throw new Error(`MockXP importNodes: Only supports "root" exports!`);
}
rootXmlNode._path = '/';
if (_trace) {
const rootNode = this.server.getNode({
branchId: this.server.context.branch,
key: UUID_NIL,
repoId: this.server.context.repository,
});
this.server.log.debug('rootNode:%s', rootNode);
}
this.server.modifyNode({
branchId: this.server.context.branch,
key: UUID_NIL,
repoId: this.server.context.repository,
editor: (node) => {
node._childOrder = rootXmlNode._childOrder;
node._indexConfig = rootXmlNode._indexConfig;
// NOTE: ASFAIK Root node can't have _manualOrderValue
if (includePermissions) {
node._inheritsPermissions = rootXmlNode._inheritsPermissions;
node._permissions = rootXmlNode._permissions;
}
// node._state = 'DEFAULT'; // Already there :)
node._ts = rootXmlNode._ts;
// NOTE: _versionKey is automatically incremented on modify
return node;
}
});
importNodesResult.updatedNodes.push(UUID_NIL);
if (_debug || _trace) {
const modifiedRootNode = this.server.getNode({
branchId: this.server.context.branch,
key: UUID_NIL,
repoId: this.server.context.repository,
});
if (_trace) {
this.server.log.debug('modifiedRootNode:%s', modifiedRootNode);
} else if (_debug) {
const { _path } = modifiedRootNode;
this.server.log.debug('updated node with _path:%s ', _path);
}
}
} // _importRootNode
private _importNode({
_debug,
_trace,
importNodesResult,
includeNodeIds,
includePermissions,
name,
parentPath,
xmlString,
}: {
_debug: boolean;
_trace: boolean;
importNodesResult: ImportNodesResult;
includeNodeIds: boolean;
includePermissions: boolean;
name: string;
parentPath: string;
xmlString: string;
}): Node | undefined {
const xmlNode = parseEnonicXml({
// _debug,
_trace,
log: this.server.log,
xmlString,
});
if (_trace) this.server.log.debug('xmlNode:%s', xmlNode);
// if (parentPath === '/' && name === 'content') {
// this.server.log.debug('contentNode:%s', xmlNode);
// }
// if (parentPath === '/content' && name === 'my-site') {
// this.server.log.debug('siteNode:%s', xmlNode);
// }
const createNodeParams: NodePropertiesOnCreate & {
_id?: string;
// data?: unknown;
// form?: unknown;
// ...
} = xmlNode;
createNodeParams._name = name;
createNodeParams._parentPath = parentPath;
if (!includeNodeIds) {
// TODO This works, but uncertain what happens to internal logics...
delete createNodeParams._id;
}
if (!includePermissions) {
delete createNodeParams._inheritsPermissions;
delete createNodeParams._permissions;
}
try {
const createdNode = this.server.createNode({
branchId: this.server.context.branch,
repoId: this.server.context.repository,
node: createNodeParams
});
if (_trace) {
this.server.log.debug('createdNode:%s', createdNode);
} else if (_debug) {
const { _path } = createdNode;
this.server.log.debug('created node with _path:%s', _path);
}
importNodesResult.addedNodes.push(createdNode._id);
return createdNode;
} catch (error) {
if (error instanceof Error) {
importNodesResult.importErrors.push({
exception: `${error}`,
message: error.message,
stacktrace: [],
});
} else {
importNodesResult.importErrors.push({
exception: `${error}`,
message: 'Unknown error',
stacktrace: [],
});
}
} // try/catch
return undefined;
} // _importNode
private _importFromExportFolder({
_debug,
_trace,
includeNodeIds,
includePermissions,
source,
}: {
_debug: boolean;
_trace: boolean;
includeNodeIds: boolean;
includePermissions: boolean;
source: string;
}) {
const exportAbsPath = resolve(this.sandboxAbsPath, 'home/data/export', source);
if (!isDirectory(exportAbsPath)) {
throw new Error(`importNodes: Export not found at ${exportAbsPath}!`);
}
const entries = listDirSync(exportAbsPath, '/');
if (_trace) this.server.log.debug('entries:%s', entries);
const firstDir = entries.shift();
if (firstDir.name !== '_') {
throw new Error(`MockXP importNodes: Only supports "root" exports!`);
}
const rootXmlString = readFileSync(join(firstDir.absPath, 'node.xml'), 'utf-8');
const importNodesResult: ImportNodesResult = {
addedNodes: [],
updatedNodes: [],
importedBinaries: [],
importErrors: [{
exception: '',
message: '',
stacktrace: [],
}],
}
this._importRootNode({
_debug,
_trace,
importNodesResult,
includePermissions,
rootXmlString,
});
function handleDirectory(this: LibExport, entries: Entry[]) {
const subDirs: Entry[][] = [];
for (
const {
absPath,
isDirectory,
name,
nodeParentPath,
// relative,
} of entries
) {
if (isDirectory) {
const xmlString = readFileSync(join(absPath, '_/node.xml'), 'utf-8');
const createdNode = this._importNode({
_debug,
_trace,
importNodesResult,
includeNodeIds,
includePermissions,
name,
parentPath: nodeParentPath,
xmlString,
});
if (createdNode) { // Count skipped subdirs as importErrors?
const subDir = listDirSync(join(absPath), createdNode._path);
if (_trace) this.server.log.debug('subDir:%s', subDir);
subDir.shift(); // Remove '_'
subDirs.push(subDir)
}
} // isDirectory
} // for entry of entries
for (const subDir of subDirs) {
handleDirectory.call(this, subDir); // Recurse
}
} // handleDirectory
handleDirectory.call(this, entries);
if (_trace) this.server.log.debug('importNodesResult:%s', importNodesResult);
return importNodesResult;
} // _importFromExportFolder
private _importFromZipFile({
_debug,
_trace,
includeNodeIds,
includePermissions,
zipFilePath,
}: {
_debug: boolean;
_trace: boolean;
includeNodeIds: boolean;
includePermissions: boolean;
zipFilePath: string
}) {
let zip: AdmZip;
let zipEntries: AdmZip.IZipEntry[];
try {
zip = new AdmZip(zipFilePath);
zipEntries = sortZipEntries(zip.getEntries(), {
prioritizeShorter: true
});
if (_trace) this.server.log.debug(`${zipFilePath} is a valid ZIP file.`);
} catch (error) {
// this.server.log.error('%s is not a valid ZIP file:%s', source, error.message);
throw new Error(`${zipFilePath} is not a valid ZIP file!`);
}
const rootEntry = zipEntries.shift();
if (_trace) this.server.log.debug('rootEntry.entryName:%s', rootEntry.entryName);
const rootXmlString = zip.readAsText(rootEntry);
if (_trace) this.server.log.debug('rootXmlString:%s', rootXmlString);
const importNodesResult: ImportNodesResult = {
addedNodes: [],
updatedNodes: [],
importedBinaries: [],
importErrors: [{
exception: '',
message: '',
stacktrace: [],
}],
}
this._importRootNode({
_debug,
_trace,
importNodesResult,
includePermissions,
rootXmlString,
});
// this.server.log.debug('entries:%s', entries);
for (const zipEntry of zipEntries) {
const {
entryName,
// header,
isDirectory
} = zipEntry;
// this.server.log.debug('entryName:%s', entryName);
const fileName = basename(entryName);
let nodePath = dirname(entryName);
const pathComponents = normalize(nodePath).split(sep);
if (pathComponents.length < 2) {
if (_trace) this.server.log.debug('pathComponents:%s', pathComponents);
continue; // Skip "top" folder with same name as zip file.
}
nodePath = `/${pathComponents.length > 1 ? pathComponents.slice(1,-1).join(sep): nodePath}`;
if (_trace) this.server.log.debug('nodePath:%s', nodePath);
const nodeParentPath = dirname(nodePath);
const nodeName = basename(nodePath);
// const fileExtension = extname(fileName).toLowerCase();
// const extensionWithoutDot = fileExtension ? fileExtension.slice(1) : 'unknown';
// this.server.log.debug('zipEntry:%s', zipEntry);
// this.server.log.debug('isDirectory:%s', isDirectory);
// this.server.log.debug('header:%s', header);
// this.server.log.debug('size:%s', header.size);
// this.server.log.debug('rawEntryName:%s', zipEntry.rawEntryName); // Array of numbers
if (!isDirectory && fileName === 'node.xml') {
const xmlString = zip.readAsText(zipEntry)
if (_trace) this.server.log.debug('xmlString:%s', xmlString);
// const createdNode =
this._importNode({
_debug,
_trace,
importNodesResult,
includeNodeIds,
includePermissions,
name: nodeName,
parentPath: nodeParentPath,
xmlString,
});
}
} // for zipEntries
return importNodesResult;
} // _importFromZipFile
constructor({
sandboxName,
server
}: {
sandboxName?: string;
server: Server;
}) {
if (sandboxName) {
this.sandboxAbsPath = resolve(homedir(),'.enonic/sandboxes', sandboxName);
if (!isDirectory(this.sandboxAbsPath)) {
throw new Error(`LibExport constructor: Sandbox not found at ${this.sandboxAbsPath}!`);
}
}
this.server = server;
} // constructor
public importNodes({
_debug = false,
_trace = false,
source, // Either name of nodes-export located in exports directory or application resource key
targetNodePath, // TODO: Target path for imported nodes
xslt, // XSLT file name in exports directory or application resource key. Used for XSLT transformation
xsltParams, // Parameters used in XSLT transformation
includeNodeIds = true, // TODO: Set to true to use node IDs from the import, false to generate new node IDs
includePermissions = false, // TODO: Set to true to use Node permissions from the import, false to use target node permissions
nodeImported, // A function to be called during import with number of nodes imported since last call
nodeResolved, // A function to be called during import with number of nodes imported since last call
}: Omit<ImportNodesParams, 'targetNodePath'> & {
_debug?: boolean;
_trace?: boolean;
targetNodePath?: string
}): ImportNodesResult {
if (targetNodePath) {
this.server.log.warning('MockXP importNodes: targetNodePath not supported yet, using "/".');
}
if (xslt) {
this.server.log.warning('MockXP importNodes: xslt ignored, not supported.');
}
if (xsltParams) {
this.server.log.warning('MockXP importNodes: xsltParams ignored, not supported.');
}
if (nodeImported) {
this.server.log.warning('MockXP importNodes: nodeImported ignored, not supported yet.');
}
if (nodeResolved) {
this.server.log.warning('MockXP importNodes: nodeResolved ignored, not supported yet.');
}
if (!isStringLiteral(source)) {
throw new Error(`importNodes: source must be a string!`);
}
if (isFile(source)) {
return this._importFromZipFile({
_debug,
_trace,
includeNodeIds,
includePermissions,
zipFilePath: source
});
}
return this._importFromExportFolder({
_debug,
_trace,
includeNodeIds,
includePermissions,
source,
});
} // importNodes
} // LibExport