marklogic
Version:
The official MarkLogic Node.js client API.
560 lines (548 loc) • 21.2 kB
JavaScript
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
;
const fs = require('fs');
const path = require('path');
const {Transform} = require('stream');
const astTypes = require('ast-types');
const b = astTypes.builders;
const {generate} = require('astring');
const Vinyl = require('./optional.js').library('vinyl');
const endpointDeclarationValidator = require('./endpointDeclarationValidator');
const endpointProxy = require('./endpoint-proxy.js');
function buildArrayLiteral(value) {
return b.arrayExpression(value.map(buildValueLiteral));
}
function buildValueLiteral(value) {
if (Array.isArray(value)) {
return buildArrayLiteral(value);
} else if (value === null || value === undefined) {
// astring.generate() doesn't seem to work well with Esprima NullLiteral
// return b.nullLiteral();
return b.literal(null);
} else if (typeof value !== 'object' || value instanceof String || value instanceof Number || value instanceof Boolean) {
return b.literal(value);
}
return buildObjectLiteral(value);
}
function buildPropertyLiteral(key, value) {
// const prop = b.property('init', b.literal(key), buildValueLiteral(value));
return b.property('init', b.literal(key), buildValueLiteral(value));
}
function buildObjectLiteral(obj) {
return b.objectExpression(
Object.entries(obj)
.map(entry => buildPropertyLiteral(entry[0], entry[1]))
);
}
function buildParam(param) {
return b.identifier(param.name);
}
function buildMethod(endpointdef) {
const functiondef = endpointdef.declaration;
const request = b.callExpression(
b.memberExpression(b.memberExpression(b.thisExpression(), b.identifier('$mlProxy')), b.identifier('execute')),
[
b.literal(functiondef.functionName),
b.objectExpression(
Array.isArray(functiondef.params) ?
functiondef.params.map(param =>
b.property('init', b.literal(param.name), b.identifier(param.name))
) :
[]
),
b.memberExpression(b.identifier('arguments'), b.identifier('length'))
]);
const method = b.methodDefinition('method', b.identifier(functiondef.functionName), b.functionExpression(null,
Array.isArray(functiondef.params) ?
functiondef.params.map(buildParam) :
[],
b.blockStatement([
b.returnStatement(request)
])
));
method.comments = [generateMethodDoc(functiondef)];
return method;
}
function chainFunctionDeclaration(prior, endpointdef) {
return b.callExpression(b.memberExpression(
prior,
b.identifier('withFunction')),
[buildValueLiteral(endpointdef.declaration), b.literal(endpointdef.moduleExtension)]);
}
function paramHasSession(foundSession, param) {
if (foundSession) {
return foundSession;
}
return (param.datatype === 'session');
}
function endpointHasSession(foundSession, endpointdef) {
if (foundSession) {
return foundSession;
}
return Array.isArray(endpointdef.declaration.params) ?
endpointdef.declaration.params.reduce(paramHasSession, foundSession) :
foundSession;
}
function buildModule(moduleName, servicedef, endpointdefs) {
const hasSession = endpointdefs.reduce(endpointHasSession, false);
const endpointMethods = endpointdefs.map(buildMethod);
const className = moduleName.substring(0, 1).toUpperCase() + moduleName.substring(1);
const strictStmt = b.expressionStatement(b.literal('use strict'));
const initializer = b.callExpression(b.memberExpression(b.identifier('client'), b.identifier('createProxy')), [
b.identifier('serviceDeclaration')
]);
strictStmt.comments = [b.line('GENERATED - DO NOT EDIT!')];
const instanceFactory = b.methodDefinition('method', b.identifier('on'), b.functionExpression(null,
[b.identifier('client'), b.identifier('serviceDeclaration')],
b.blockStatement([
b.returnStatement(b.newExpression(b.identifier(className),
[b.identifier('client'), b.identifier('serviceDeclaration')]))
])), true);
instanceFactory.comments = [generateInstanceFactoryDoc(className)];
// modifies the endpoint declarations so must appear after other uses of the endpoint declarations
endpointdefs.forEach(endpointProxy.expandEndpointDeclaration);
const endpointSerializations = endpointdefs.reduce(chainFunctionDeclaration, initializer);
const constructor =
b.methodDefinition('constructor', b.identifier('constructor'), b.functionExpression(null,
[b.identifier('client'), b.identifier('serviceDeclaration')],
b.blockStatement([
b.ifStatement(b.logicalExpression('||',
b.binaryExpression('===', b.identifier('client'), b.identifier('undefined')),
b.binaryExpression('===', b.identifier('client'), b.literal(null))
),
b.blockStatement([b.throwStatement(b.newExpression(b.identifier('Error'), [
b.literal('missing required client')
]))])),
b.ifStatement(b.logicalExpression('||',
b.binaryExpression('===', b.identifier('serviceDeclaration'), b.identifier('undefined')),
b.binaryExpression('===', b.identifier('serviceDeclaration'), b.literal(null))
),
b.blockStatement([b.expressionStatement(
b.assignmentExpression('=', b.identifier('serviceDeclaration'), buildValueLiteral(servicedef))
)])),
b.expressionStatement(b.assignmentExpression('=',
b.memberExpression(b.thisExpression(), b.identifier('$mlProxy')),
endpointSerializations
))
]))
);
constructor.comments = [generateConstructorDoc(className)];
let sessionFactory = null;
if (hasSession) {
const sessionFactoryName = 'createSession';
sessionFactory = b.methodDefinition('method', b.identifier(sessionFactoryName),
b.functionExpression(null, [], b.blockStatement([
b.returnStatement(b.callExpression(
b.memberExpression(b.memberExpression(b.thisExpression(), b.identifier('$mlProxy')), b.identifier('createSession')),
[]))
]))
);
sessionFactory.comments = [generateSessionFactoryDoc(className, sessionFactoryName)];
}
const classMethods = hasSession ? [instanceFactory, constructor, sessionFactory] : [instanceFactory, constructor];
const classStmt = b.classDeclaration(
b.identifier(className),
b.classBody(classMethods.concat(endpointMethods))
);
classStmt.comments = [generateClassDoc(servicedef)];
return b.program([
strictStmt,
classStmt,
b.expressionStatement(b.assignmentExpression('=',
b.memberExpression(b.identifier('module'), b.identifier('exports')),
b.identifier(className)
))
]);
}
function generateClassDoc(servicedef) {
const serviceDesc = servicedef.desc;
const serviceDoc = (serviceDesc !== void 0 && serviceDesc !== null) ? serviceDesc :
'Provides a set of operations on the database server';
return b.commentBlock(`*
* ${serviceDoc}
`);
}
function generateInstanceFactoryDoc(className) {
const paramDoc = generateConstructorParamDoc();
return b.commentBlock(`*
* A convenience factory that calls the constructor to create the ${className} object for executing operations
* on the database server.
* ${paramDoc}
* @returns {${className}} the object for the database operations
`);
}
function generateConstructorDoc(className) {
const paramDoc = generateConstructorParamDoc();
return b.commentBlock(`*
* The constructor for creating a ${className} object for executing operations on the database server.
* ${paramDoc}
`);
}
function generateConstructorParamDoc() {
return `@param {DatabaseClient} client - the client for accessing the database server as the user
* @param {object} [serviceDeclaration] - an optional declaration for a custom implementation of the service`;
}
function generateMethodDoc(functiondef) {
const functionDesc = functiondef.desc;
const functionDoc = (functionDesc !== void 0 && functionDesc !== null) ? functionDesc :
`Invokes the ${functiondef.functionName} operation on the database server.`;
const paramsDoc = Array.isArray(functiondef.params) ?
functiondef.params.map(generateParamDoc) : '';
const returnDoc = generateReturnDoc(functiondef);
return b.commentBlock(`*
* ${functionDoc}${paramsDoc}${returnDoc}
`);
}
function generateParamDoc(paramdef) {
const paramName = paramdef.name;
const paramDesc = paramdef.desc;
const datatype = paramdef.datatype;
const multiple = paramdef.multiple === true;
const nullable = paramdef.nullable === true;
const docname = nullable ? `[${paramName}]` : paramName;
let doctype = null;
switch (datatype) {
case 'boolean':
doctype = '{boolean|string}';
break;
case 'dateTime':
doctype = '{Date|string}';
break;
case 'decimal':
case 'double':
case 'float':
case 'int':
case 'long':
case 'unsignedInt':
case 'unsignedLong':
doctype = '{number|string}';
break;
case 'date':
case 'dayTimeDuration':
case 'string':
case 'time':
doctype = '{string}';
break;
case 'binaryDocument':
doctype = '{Buffer|stream.Readable}';
break;
case 'array':
doctype = '{array|Buffer|Set|stream.Readable|string}';
break;
case 'jsonDocument':
doctype = '{array|Buffer|Map|Set|stream.Readable|string}';
break;
case 'object':
doctype = '{Buffer|Map|stream.Readable|string}';
break;
case 'textDocument':
case 'xmlDocument':
doctype = '{Buffer|stream.Readable|string}';
break;
case 'session':
doctype = '{SessionState}';
break;
default:
throw new Error('invalid datatype configuration: ' + datatype);
}
const paramDoc = (paramDesc !== void 0 && paramDesc !== null) ? paramDesc :
multiple ? `provides multiple input values of the ${datatype} datatype` :
`provides an input value of the ${datatype} datatype`;
return `
* @param ${doctype} ${docname} - ${paramDoc}`;
}
function generateReturnDoc(functiondef) {
let returnClass = 'Promise';
const outputMode = functiondef.$jsOutputMode;
if (outputMode !== void 0 && outputMode !== null) {
switch(outputMode.toLowerCase()) {
case 'promise':
break;
case 'stream':
returnClass = 'stream.Readable';
break;
default:
throw new Error('$jsOutputMode not "promise" or "stream": '+outputMode);
}
}
const returndef = functiondef.return;
let returnDoc = null;
if (returndef === void 0 || returndef === null) {
returnDoc = 'for success or failure';
} else {
const returnDesc = returndef.desc;
if (returnDesc !== void 0 && returnDesc !== null) {
returnDoc = returnDesc;
} else {
const datatype = returndef.datatype;
const multiple = returndef.multiple === true;
let jsType = functiondef.$jsType;
if (jsType === void 0 || jsType === null) {
switch (datatype) {
case 'boolean':
jsType = 'boolean';
break;
case 'date':
case 'dayTimeDuration':
case 'dateTime':
case 'decimal':
case 'double':
case 'long':
case 'string':
case 'textDocument':
case 'time':
case 'unsignedLong':
case 'xmlDocument':
jsType = 'string';
break;
case 'float':
case 'int':
case 'unsignedInt':
jsType = 'number';
break;
case 'array':
jsType = 'array';
break;
case 'binaryDocument':
jsType = 'Buffer';
break;
case 'jsonDocument':
case 'object':
jsType = 'object';
break;
default:
throw new Error(`unknown datatype ${datatype} for documenting return value`);
}
}
returnDoc = multiple ? `multiple ${jsType} values of the ${datatype} data type` :
`${jsType} value of the ${datatype} data type`;
}
}
return `
* @returns {${returnClass}} ${returnDoc}`;
}
function generateSessionFactoryDoc(className, sessionFactoryName) {
return b.commentBlock(`*
* A factory for creating a SessionsState object for maintaining a
* server session across multiple calls to the methods of the ${className}
* class that take a SessionsState parameter.
* @method ${className}#${sessionFactoryName}
* @returns {SessionsState} the session state for the database operations
`);
}
function generateModuleSource(moduleName, servicedef, endpointdefs) {
if (moduleName === void 0 || moduleName === null) {
throw new Error('missing module name');
} else if (servicedef === void 0 || servicedef === null) {
throw new Error('missing service.json declaration');
} else if (servicedef.endpointDirectory === void 0 || servicedef.endpointDirectory === null) {
throw new Error('service.json declaration without endpointDirectory property');
} else if (!Array.isArray(endpointdefs) || endpointdefs.length === 0) {
throw new Error('no endpoint pairs of *.api declaration and main module');
} else {
endpointdefs.forEach(endpoint => {
if (endpoint.moduleExtension === void 0 || endpoint.moduleExtension === null) {
throw new Error('endpoint without moduleExtension property');
} else if (endpoint.declaration === void 0 || endpoint.declaration === null) {
throw new Error('endpoint without declaration');
} else if (endpoint.declaration.functionName === void 0 || endpoint.declaration.functionName === null) {
throw new Error('endpoint declaration without functionName property');
}
const validationResult = endpointDeclarationValidator.validate(endpoint.declaration);
if (!(validationResult.isValid)) {
throw new Error(`invalid endpoint declaration ${validationResult.errors}`);
}
});
}
const proxyAST = buildModule(moduleName, servicedef, endpointdefs);
return generate(proxyAST, {comments: true});
}
class DirectoryDeclarationReader {
constructor(directory, success, failure) {
this._directory = directory;
this._success = success;
this._failure = failure;
this._serviceDeclaration = null;
this._endpoints = null;
this._fileQueue = null;
this._fileNext = -1;
this.readDirectory();
}
success(result) {
this._success(result);
}
failure(err) {
this._failure(err);
}
readDirectory() {
const directory = this._directory;
fs.readdir(directory, (err, filenames) => {
if (err) {
this.failure(err);
} else if (filenames.length === 0) {
this.success(null);
} else {
const endpoints = new Map();
this._fileQueue = filenames
.filter(filename => {
const extension = path.extname(filename);
const basename = path.basename(filename, extension);
switch (extension) {
case '.api':
if (!endpoints.has(basename)) {
endpoints.set(basename, {declaration: null, moduleExtension: null});
}
return true;
case '.json':
return (basename === 'service');
case '.mjs':
case '.sjs':
case '.xqy':
if (!endpoints.has(basename)) {
endpoints.set(basename, {declaration: null, moduleExtension: extension});
} else {
const endpoint = endpoints.get(basename);
if (endpoint.moduleExtension !== null) {
console.warn(`skipping redundant main module ${filename} in ${directory}`);
} else {
endpoint.moduleExtension = extension;
}
}
return false;
default:
return false;
}})
.map(filename => path.resolve(directory, filename));
this._endpoints = endpoints;
this._fileNext = 0;
this.readFile();
}
});
}
readFile() {
const fileQueue = this._fileQueue;
const endpoints = this._endpoints;
if (this._fileNext < fileQueue.length) {
const filename = fileQueue[this._fileNext++];
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
this.failure(err);
return;
}
const extension = path.extname(filename);
const basename = path.basename(filename, extension);
switch(extension) {
case '.json':
if (basename === 'service') {
this._serviceDeclaration = JSON.parse(data);
} else {
console.warn(`unknown JSON file: ${filename}`);
}
break;
case '.api':
endpoints.get(basename).declaration = JSON.parse(data);
break;
default:
console.warn(`unknown file: ${filename}`);
break;
}
this.readFile();
});
} else {
this.generateResult();
}
}
generateResult() {
const directory = this._directory;
const serviceDeclaration = this._serviceDeclaration;
const endpoints = this._endpoints;
if (serviceDeclaration === null) {
console.warn(`skipping directory without service.json: ${directory}`);
this.success(null);
} else if (endpoints.size === 0) {
console.warn(`skipping directory without endpoints: ${directory}`);
this.success(null);
} else {
const functionDeclarations = Array
.from(endpoints)
.filter(endpoint => {
if (endpoint[1].declaration === null || endpoint[1].moduleExtension === null) {
console.warn(`skipping incomplete endpoint ${endpoint[0]} in directory: ${directory}`);
return false;
}
return true;
})
.map(endpoint => endpoint[1]);
if (functionDeclarations.length === 0) {
console.warn(`skipping directory without complete endpoints: ${directory}`);
this.success(null);
} else {
const result = {serviceDeclaration:serviceDeclaration, functionDeclarations:functionDeclarations};
this.success(result);
}
}
}
}
function readDeclarationsFromDirectory(directory) {
return new Promise((success, failure) => new DirectoryDeclarationReader(directory, success, failure));
}
function transformDirectoryToSource() {
return new Transform({
allowHalfOpen: false,
writableObjectMode: true,
readableObjectMode: true,
transform(directory, encoding, callback) {
if (!Vinyl.isVinyl(directory) || !directory.isDirectory()) {
console.trace('not Vinyl directory: ' +
(Vinyl.isVinyl(directory) ? directory.path : directory.toString())
);
callback();
return;
}
readDeclarationsFromDirectory(directory.path)
.then(result => {
if (result === void 0 || result === null ||
!Array.isArray(result.functionDeclarations) || result.functionDeclarations.length === 0
) {
console.trace('no declarations: ' +directory.path);
callback();
return;
}
const serviceDeclaration = result.serviceDeclaration;
const cwd = directory.cwd;
const outputBase = path.resolve(cwd, 'lib');
const moduleRelative = serviceDeclaration.$jsModule;
let outputRelative = null;
let outputModule = null;
if (moduleRelative === void 0 || moduleRelative === null || moduleRelative.length === 0) {
outputModule = directory.basename;
outputRelative = outputModule+'.js';
} else {
const moduleBasename = path.basename(moduleRelative);
const extensionLen = path.extname(moduleBasename).length;
outputModule = (extensionLen === 0) ? moduleBasename :
moduleBasename.substring(0, moduleBasename.length - extensionLen);
outputRelative = moduleRelative;
}
const proxySrc = generateModuleSource(outputModule, serviceDeclaration, result.functionDeclarations);
const outputPath = path.resolve(outputBase, outputRelative);
const outputFile = {
cwd: cwd,
base: outputBase,
path: outputPath,
contents: Buffer.from(proxySrc, 'utf-8')
};
callback(null, new Vinyl(outputFile));
})
.catch(callback);
}
});
}
module.exports = {
generateSource: generateModuleSource,
readDeclarations: readDeclarationsFromDirectory,
generate: transformDirectoryToSource,
validate: endpointDeclarationValidator.validate
};