UNPKG

marklogic

Version:

The official MarkLogic Node.js client API.

845 lines (759 loc) 27.7 kB
/* * Copyright © 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ 'use strict'; const Operation = require('./operation.js'); const mlutil = require('./mlutil.js'); const requester = require('./requester.js'); const SessionState = require('./session-state.js'); function emptyRequest(client, funcdef, args) { const endpoint = funcdef.endpoint(); const requestHeaders = {}; const sessionParamName = checkSessionParam(funcdef, args, requestHeaders); const argNames = Object.keys(args); if (argNames.length > 1 || (argNames.length === 1 && argNames[0] !== sessionParamName)) { throw new Error('function does not take arguments: ' + funcdef.name()); } const outputTransform = specifyOutputTransform(funcdef, requestHeaders); const requestOptions = mlutil.newRequestOptions(client.getConnectionParams(), endpoint, 'POST'); requestOptions.headers = requestHeaders; const operation = new Operation( 'call to ' + endpoint, client, requestOptions, 'empty', funcdef.returnKind() ); addOutputTransform(funcdef, operation, outputTransform); return startRequest(funcdef, operation); } function multiAtomicRequest(client, funcdef, args) { const endpoint = funcdef.endpoint(); const requestHeaders = { 'Content-Type': 'application/x-www-form-urlencoded;charset="utf-8"' }; checkArgNames(funcdef, args, requestHeaders); const requestOptions = mlutil.newRequestOptions(client.getConnectionParams(), endpoint, 'POST'); requestOptions.headers = requestHeaders; const operation = new Operation( 'call to ' + endpoint, client, requestOptions, 'single', funcdef.returnKind() ); const requestPartList = []; for (const [paramName, paramdef] of funcdef.paramdefs()) { const paramArgs = args[paramName]; if (isArgNull(paramName, paramdef, paramArgs)) { continue; } if (Array.isArray(paramArgs)) { for (const paramArg of paramArgs) { requestPartList.push(paramName + '=' + encodeURIComponent(mlutil.marshal(paramArg, operation))); } } else { requestPartList.push(paramName + '=' + encodeURIComponent(mlutil.marshal(paramArgs, operation))); } } operation.requestBody = requestPartList.join('&'); const outputTransform = specifyOutputTransform(funcdef, requestHeaders); addOutputTransform(funcdef, operation, outputTransform); return startRequest(funcdef, operation); } function multiNodeRequest(client, funcdef, args) { const endpoint = funcdef.endpoint(); const multipartBoundary = mlutil.multipartBoundary; const requestHeaders = { 'Content-Type': 'multipart/form-data; boundary=' + multipartBoundary }; checkArgNames(funcdef, args, requestHeaders); const requestOptions = mlutil.newRequestOptions(client.getConnectionParams(), endpoint, 'POST'); requestOptions.headers = requestHeaders; const operation = new Operation( 'call to ' + endpoint, client, requestOptions, 'multipart', funcdef.returnKind() ); operation.multipartBoundary = multipartBoundary; const requestPartList = []; for (const [paramName, paramdef] of funcdef.paramdefs()) { const paramArgs = args[paramName]; if (isArgNull(paramName, paramdef, paramArgs)) { continue; } const headers = { 'Content-Type': paramdef.mimeType(), 'Content-Disposition': 'form-data; name="' + paramName + '"' }; if (paramdef.multiple()) { for (const paramArg of paramArgs) { requestPartList.push({ headers: headers, content: mlutil.marshal(paramArg, operation) }); } } else { requestPartList.push({ headers: headers, content: mlutil.marshal(paramArgs, operation) }); } } operation.requestPartList = requestPartList; const outputTransform = specifyOutputTransform(funcdef, requestHeaders); addOutputTransform(funcdef, operation, outputTransform); return startRequest(funcdef, operation); } function startRequest(funcdef, operation) { switch(funcdef.outputMode()) { case 'promise': return requester.startRequest(operation).result('promise'); case 'stream': return requester.startRequest(operation).stream('object'); default: throw new Error('unknown output mode: ' + funcdef.outputMode()); } } function addSessionParam(sessionParam, sessionArg, requestHeaders) { if (sessionArg === void 0 || sessionArg === null) { return; } else if (!(sessionArg instanceof SessionState)) { throw new Error('unknown value for ' + sessionParam + ' session parameter: ' + sessionArg); } requestHeaders.Cookie = 'SessionID=' + sessionArg.sessionId(); } function addOutputTransform(funcdef, operation, outputTransform) { if (outputTransform !== null) { operation.outputTransform = outputTransform; operation.inlineAsDocument = false; operation.returndef = funcdef.returndef(); } } function checkArgNames(funcdef, args, requestHeaders) { if (typeof args !== 'object' || args === null) { throw new Error('argument not an object for: ' + funcdef.name()); } const sessionParamName = checkSessionParam(funcdef, args, requestHeaders); const paramdefs = funcdef.paramdefs(); Object.keys(args).forEach(argName => { if (!paramdefs.has(argName)) { if (sessionParamName === null || argName !== sessionParamName) { throw new Error('argument for unknown parameter: ' + argName); } } }); } function checkSessionParam(funcdef, args, requestHeaders) { const sessionParam = funcdef.sessionParam(); if (sessionParam === null) { return null; } const sessionParamName = sessionParam.name(); const sessionArg = args[sessionParamName]; if (sessionArg !== void 0 && sessionArg !== null) { addSessionParam(sessionParamName, sessionArg, requestHeaders); } else if (!sessionParam.nullable()) { throw new Error('missing required session parameter: ' + sessionParamName); } return sessionParamName; } function isArgNull(paramName, paramdef, paramArgs) { if (paramArgs === void 0 || paramArgs === null) { if (!paramdef.nullable()) { throw new Error('null value not allowed for parameter: ' + paramName); } return true; } if (Array.isArray(paramArgs)) { if (!paramdef.multiple()) { const paramDatatype = paramdef.datatype(); if (paramDatatype !== 'array' && paramDatatype !== 'jsonDocument') { throw new Error('multiples values not allowed for parameter: ' + paramName); } } else if (paramArgs.length === 0) { if (!paramdef.nullable()) { throw new Error('empty value list not allowed for parameter: ' + paramName); } return true; } } return false; } function specifyOutputTransform(funcdef, requestHeaders) { switch (funcdef.returnKind()) { case 'empty': return null; case 'single': const returndef = funcdef.returndef(); requestHeaders.Accept = returndef.mimeType(); switch (returndef.dataKind()) { case 'atomic': return singleAtomicOutputTransform; case 'node': return singleNodeOutputTransform; default: throw new Error('unknown kind of data for single return value: ' + returndef.dataKind()); } break; case 'multipart': requestHeaders.Accept = 'multipart/mixed; boundary=' + mlutil.multipartBoundary; return multiOutputTransform; default: throw new Error('unknown kind of return: ' + funcdef.returnKind()); } } function singleAtomicOutputTransform(headers, data) { /*jshint validthis:true */ const operation = this; const returndef = operation.returndef; if (isNullValue(operation, returndef, data)) { return null; } else if (isMultipleValue(operation, returndef, data)) { operation.errorListener('multiple values returned for single atomic response'); return null; } return convertAtomicValue(operation, returndef, data); } function singleNodeOutputTransform(headers, data) { /*jshint validthis:true */ const operation = this; const returndef = operation.returndef; if (isNullValue(operation, returndef, data)) { return null; } else if (isMultipleValue(operation, returndef, data)) { operation.errorListener('multiple values returned for single node response'); return null; } return data; } function multiOutputTransform(headers, data) { /*jshint validthis:true */ const operation = this; const returndef = operation.returndef; if (isNullValue(operation, returndef, data)) { return null; } return Array.isArray(data) ? data.map(value => convertValue(operation, returndef, value)) : convertValue(operation, returndef, data); } function isNullValue(operation, returndef, value) { if (value === void 0 || value === null) { if (returndef !== null && !returndef.nullable()) { operation.errorListener('invalid null response'); } return true; } else if (returndef === null) { operation.errorListener('invalid value response'); return true; } return false; } function isMultipleValue(operation, returndef, value) { if (Array.isArray(value)) { if (returndef.datatype() === 'array') { return false; } else if (!returndef.multiple()) { operation.errorListener('invalid multiple response'); } return true; } return false; } function convertValue(operation, returndef, value) { if (value === null) { return value; } const returnKind = returndef.dataKind(); switch (returnKind) { case 'atomic': return convertAtomicValue(operation, returndef, value); case 'node': return value; default: throw new Error(`unknown return kind ${returnKind}`); } } function convertAtomicValue(operation, returndef, value) { if (value === null) { return value; } switch (returndef.getJsType()) { case 'boolean': return 'true' === value; case 'number': const converted = Number(value); if (Number.isNaN(converted)) { operation.errorListener('response not numeric: '+value); return null; } return converted; case 'string': return value; case 'Date': return new Date(value); default: throw new Error( `unknown JavaScript type ${returndef.getJsType()} for atomic datatype ${returndef.datatype()}` ); } } function paramMapEntry(paramDeclaration) { const paramdef = new Paramdef(paramDeclaration); return [paramdef.name(), paramdef]; } // START SHARED WITH Modules/MarkLogic/rest-api/lib/openapi-transform.sjs IN THE SERVER function expandDataDeclaration(id, dataDeclaration) { const datatype = dataDeclaration.datatype; if (datatype === void 0 || datatype === null) { throw new Error(`missing datatype for ${id}`); } switch (datatype) { case 'boolean': case 'date': case 'dateTime': case 'dayTimeDuration': case 'decimal': case 'double': case 'float': case 'int': case 'long': case 'string': case 'time': case 'unsignedInt': case 'unsignedLong': dataDeclaration.dataKind = 'atomic'; dataDeclaration.mimeType = 'text/plain'; break; case 'binaryDocument': dataDeclaration.dataKind = 'node'; dataDeclaration.mimeType = 'application/x-unknown-content-type'; break; case 'array': case 'jsonDocument': case 'object': dataDeclaration.dataKind = 'node'; dataDeclaration.mimeType = 'application/json'; break; case 'textDocument': dataDeclaration.dataKind = 'node'; dataDeclaration.mimeType = 'text/plain'; break; case 'xmlDocument': dataDeclaration.dataKind = 'node'; dataDeclaration.mimeType = 'application/xml'; break; default: throw new Error(`invalid datatype configuration ${datatype} for ${id}`); } const multiple = dataDeclaration.multiple; if (multiple === void 0 || multiple === null) { dataDeclaration.multiple = false; } else if (typeof multiple !== 'boolean' && !(multiple instanceof Boolean)) { throw new Error(`invalid multiple configuration ${multiple} for ${id}`); } else if (multiple === true && datatype === 'session') { throw new Error(`session cannot be multiple for ${id}`); } const nullable = dataDeclaration.nullable; if (nullable === void 0 || nullable === null) { dataDeclaration.nullable = false; } else if (typeof nullable !== 'boolean' && !(nullable instanceof Boolean)) { throw new Error(`invalid nullable configuration ${nullable} for ${id}`); } } function expandParamDeclaration(paramDeclaration) { const paramName = paramDeclaration.name; if (paramName === void 0 || paramName === null) { throw new Error(`missing parameter name`); } expandDataDeclaration('${paramName} parameter', paramDeclaration); } function expandReturnDeclaration(returnDeclaration) { expandDataDeclaration('return value', returnDeclaration); let jstype = returnDeclaration.$jsType; if (jstype === void 0) { jstype = null; } const datatype = returnDeclaration.datatype; switch (datatype) { case 'boolean': if (jstype === null) { returnDeclaration.$jsType = 'boolean'; } else if (jstype !== 'boolean' && jstype !== 'string') { throw new Error( `optional $jsType ${jstype} can only be "boolean" or "string" for datatype ${datatype} return value` ); } break; case 'date': case 'dayTimeDuration': case 'decimal': case 'double': case 'long': case 'string': case 'time': case 'unsignedLong': if (jstype === null) { returnDeclaration.$jsType = 'string'; } else if (jstype !== 'string') { throw new Error( `optional $jsType ${jstype} can only be "string" for datatype ${datatype} return value` ); } break; case 'dateTime': if (jstype === null) { returnDeclaration.$jsType = 'string'; } else if (jstype !== 'Date' && jstype !== 'string') { throw new Error( `optional $jsType ${jstype} can only be "Date" or "string" for datatype ${datatype} return value` ); } break; case 'float': case 'int': case 'unsignedInt': if (jstype === null) { returnDeclaration.$jsType = 'number'; } else if (jstype !== 'number' && jstype !== 'string') { throw new Error( `optional $jsType ${jstype} can only be "number" or "string" for datatype ${datatype} return value` ); } break; case 'array': case 'binaryDocument': case 'jsonDocument': case 'object': case 'textDocument': case 'xmlDocument': if (jstype !== null) { throw new Error(`cannot specify a $jsType for datatype ${datatype} for return value`); } break; default: throw new Error(`unknown datatype ${datatype} for return value`); } } function expandFunctionDeclaration(functionDeclaration) { const functionName = functionDeclaration.functionName; if (functionName === void 0 || functionName === null) { throw new Error(`missing function name`); } let paramsKind = 'empty'; let sessionParam = null; const paramDeclarations = functionDeclaration.params; if (paramDeclarations === void 0 || paramDeclarations === null) { functionDeclaration.params = []; functionDeclaration.maxArgs = 0; } else if (paramDeclarations.length > 0) { const filteredDeclarations = paramDeclarations.filter(paramDeclaration => { if (paramDeclaration === void 0 || paramDeclaration === null) { return false; } else if (paramDeclaration.datatype === 'session') { if (sessionParam !== null) { throw new Error( `${functionName} function with multiple session parameters: ${sessionParam.name} and ${paramDeclaration.name}` ); } sessionParam = paramDeclaration; return false; } expandParamDeclaration(paramDeclaration); switch (paramsKind) { case 'empty': const dataKind = paramDeclaration.dataKind; switch (dataKind) { case 'atomic': paramsKind = 'multiAtomic'; break; case 'node': paramsKind = 'multiNode'; break; default: throw new Error( `invalid kind of param data ${dataKind} for ${paramDeclaration.name} parameter` ); } break; case 'multiAtomic': if (paramDeclaration.dataKind === 'node') { paramsKind = 'multiNode'; } break; case 'multiNode': break; default: throw new Error( `invalid kind of parameter list ${paramsKind} for ${paramDeclaration.name} parameter` ); } return true; }); if (filteredDeclarations.length < paramDeclarations.length) { functionDeclaration.params = filteredDeclarations; functionDeclaration.maxArgs = (sessionParam !== null) ? (filteredDeclarations.length + 1) : filteredDeclarations.length; } else { functionDeclaration.maxArgs = paramDeclarations.length; } } functionDeclaration.paramsKind = paramsKind; if (sessionParam !== null) { sessionParam.dataKind = 'session'; sessionParam.mimeType = null; } functionDeclaration.sessionParam = sessionParam; const returnDeclaration = functionDeclaration.return; if (returnDeclaration === void 0) { functionDeclaration.return = null; functionDeclaration.returnKind = 'empty'; } else if (returnDeclaration === null) { functionDeclaration.returnKind = 'empty'; } else { expandReturnDeclaration(returnDeclaration); functionDeclaration.returnKind = returnDeclaration.multiple ? 'multipart' : 'single'; } const outputMode = functionDeclaration.$jsOutputMode; if (outputMode === void 0 || outputMode === null) { functionDeclaration.$jsOutputMode = 'promise'; } else if (outputMode !== 'promise' && outputMode !== 'stream') { throw new Error(`${functionName} must have $jsOutputMode of "promise" or "stream" instead of ${outputMode}`); } } function expandEndpointDeclaration(endpointDeclaration) { const moduleExtension = endpointDeclaration.moduleExtension; if (typeof moduleExtension !== "string" || moduleExtension.length === 0) { throw new Error('invalid module extension configuration: ' + moduleExtension); } else if (!moduleExtension.startsWith('.')) { endpointDeclaration.moduleExtension = '.' + moduleExtension; } expandFunctionDeclaration(endpointDeclaration.declaration); } // END SHARED WITH Modules/MarkLogic/rest-api/lib/openapi-transform.sjs IN THE SERVER class Datadef { constructor(dataDeclaration) { this._dataKind = dataDeclaration.dataKind; this._datatype = dataDeclaration.datatype; this._mimeType = dataDeclaration.mimeType; this._multiple = dataDeclaration.multiple; this._nullable = dataDeclaration.nullable; } dataKind() { return this._dataKind; } mimeType() { return this._mimeType; } datatype() { return this._datatype; } multiple() { return this._multiple; } nullable() { return this._nullable; } } class Paramdef extends Datadef { constructor(paramDeclaration) { super(paramDeclaration); this._name = paramDeclaration.name; } name() { return this._name; } } class Returndef extends Datadef { constructor(returnDeclaration) { super(returnDeclaration); this._jstype = returnDeclaration.$jsType; } getJsType() { return this._jstype; } } class Functiondef { constructor(functionDeclaration, moduleDirectory, moduleExtension) { const funcName = functionDeclaration.functionName; this._name = funcName; this._endpoint = moduleDirectory + funcName + moduleExtension; const params = functionDeclaration.params; this._maxArgs = functionDeclaration.maxArgs; this._paramdefs = (params !== null) ? new Map(params.map(paramMapEntry)) : new Map(); this._paramsKind = functionDeclaration.paramsKind; const sessionParam = functionDeclaration.sessionParam; this._sessionParam = (sessionParam !== null) ? new Paramdef(sessionParam) : null; const returndef = functionDeclaration.return; this._returndef = (returndef !== null) ? new Returndef(returndef) : null; this._returnKind = functionDeclaration.returnKind; this._outputMode = functionDeclaration.$jsOutputMode; } name() { return this._name; } endpoint() { return this._endpoint; } maxArgs() { return this._maxArgs; } paramdefs() { return this._paramdefs; } paramsKind() { return this._paramsKind; } sessionParam() { return this._sessionParam; } returndef() { return this._returndef; } returnKind() { return this._returnKind; } outputMode() { return this._outputMode; } } class EndpointProxy { constructor(client, serviceDeclaration) { if (client === void 0 || client === null) { throw new Error('no database client provided'); } if (serviceDeclaration === void 0 || serviceDeclaration === null) { throw new Error('no service declaration provided'); } const db = client.getConnectionParams().database; if (db !== void 0 && db !== null) { throw new Error('connection for client cannot specify database'); } this._client = client; const endpointDirectory = serviceDeclaration.endpointDirectory; if (!(typeof endpointDirectory === 'string' || (endpointDirectory instanceof String))) { throw new Error('service declaration must specify endpoint directory'); } this._endpointDirectory = endpointDirectory.endsWith('/') ? endpointDirectory : endpointDirectory + '/'; const endpointExtension = serviceDeclaration.endpointExtension; if (typeof endpointExtension === 'string' || (endpointExtension instanceof String)) { this._moduleExtension = (endpointDirectory.length === 0) ? null : endpointExtension.startsWith('.') ? endpointExtension : '.' + endpointExtension; } else { this._moduleExtension = null; } this._functiondefs = new Map(); } withFunction(functionDeclaration, moduleExtension) { const overrideExtension = this._moduleExtension; const funcdef = new Functiondef( functionDeclaration, this._endpointDirectory, (overrideExtension !== null) ? overrideExtension : moduleExtension ); this._functiondefs.set(funcdef.name(), funcdef); return this; } createSession() { return new SessionState(); } execute(functionName, args, arglen) { if (!(typeof functionName === 'string' || (functionName instanceof String))) { throw new Error('call without function name'); } const funcdef = this._functiondefs.get(functionName); if (funcdef === void 0) { throw new Error('no function definition for function: ' + functionName); } if (args === void 0 || args === null) { args = {}; } else if (typeof args !== 'object') { throw new Error('call without arguments object: ' + args); } if (arglen > funcdef.maxArgs()) { throw new Error('call with more arguments than parameters: ' + arglen); } switch (funcdef.paramsKind()) { case 'empty': return emptyRequest(this._client, funcdef, args); case 'multiAtomic': return multiAtomicRequest(this._client, funcdef, args); case 'multiNode': return multiNodeRequest(this._client, funcdef, args); default: throw new Error('unknown kind of parameter list for function: ' + functionName); } } } class ServiceCaller { constructor(client, serviceDeclaration, endpointDeclarations) { if (client === void 0 || client === null) { throw new Error('no database client provided'); } if (serviceDeclaration === void 0 || serviceDeclaration === null) { throw new Error('no service declaration provided'); } else if (!(typeof serviceDeclaration.endpointExtension === 'string' || (serviceDeclaration.endpointExtension instanceof String))) { throw new Error('serviceDeclaration.endpointExtension must be a String'); } if(endpointDeclarations === null || !(Array.isArray(endpointDeclarations)) || endpointDeclarations.length === 0) { throw new Error('endpointDeclarations needs to be a non-empty array.'); } this.$mlProxy = endpointDeclarations.reduce((proxy, endpointDeclarations) => { expandFunctionDeclaration(endpointDeclarations); return proxy.withFunction(endpointDeclarations, serviceDeclaration.endpointExtension); }, client.createProxy(serviceDeclaration) ); } call(functionName, args) { return this.$mlProxy.execute(functionName, args, 0); } } class EndpointCaller { constructor(client, endpointDeclaration) { if (client === void 0 || client === null) { throw new Error('no database client provided'); } if (endpointDeclaration === void 0 || endpointDeclaration === null) { throw new Error('endpointDeclaration cannot be null.'); } const endpoint = endpointDeclaration.endpoint; if (!(typeof endpoint === 'string' || (endpoint instanceof String)) || endpoint.length === 0) { throw new Error('endpointDeclaration endpoint needs to be a non empty string.'); } const regex = /^(.*[/])([^/]+)(\.[^/.]+)$/; const arrayValues = endpoint.match(regex); if(!(Array.isArray(arrayValues)) || (arrayValues.length !== 4)) { throw new Error('endpointDeclaration endpoint must contain endpointDirectory, functionName and extension.'); } const endpointDirectory = arrayValues[1]; const endpointFunctionName = arrayValues[2]; const endpointExtension = arrayValues[3]; if (!(typeof endpointDirectory === 'string' || (endpointDirectory instanceof String)) || endpointDirectory.length === 0){ throw new Error('endpointDirectory needs to be a non empty string'); } if (!(typeof endpointFunctionName === 'string' || (endpointFunctionName instanceof String)) || endpointFunctionName.length === 0){ throw new Error('endpointFunctionName needs to be a non empty string'); } if (!(typeof endpointExtension === 'string' || (endpointExtension instanceof String)) || endpointExtension.length === 0){ throw new Error('endpointExtension needs to be a non empty string'); } this.functionName = endpointFunctionName; const serviceDeclaration = {endpointDirectory: endpointDirectory, endpointExtension: endpointExtension}; const copyEndpointDeclaration = mlutil.copyProperties(endpointDeclaration); copyEndpointDeclaration.functionName = endpointFunctionName; expandFunctionDeclaration(copyEndpointDeclaration); this.$mlProxy = client.createProxy(serviceDeclaration).withFunction(copyEndpointDeclaration, endpointExtension); } call(args) { return this.$mlProxy.execute(this.functionName, args, 0); } } function makeEndpointProxy(dbClient, serviceDeclaration) { return new EndpointProxy(dbClient, serviceDeclaration); } function initService(dbClient, serviceDeclaration, endpointDeclarations) { return new ServiceCaller(dbClient, serviceDeclaration, endpointDeclarations); } function initEndpoint(dbClient, endpointDeclaration) { return new EndpointCaller(dbClient, endpointDeclaration); } module.exports = { init: makeEndpointProxy, expandEndpointDeclaration: expandEndpointDeclaration, initService: initService, initEndpoint: initEndpoint };