UNPKG

oracle-nosqldb

Version:

Node.js driver for Oracle NoSQL Database

555 lines (498 loc) 18.3 kB
/*- * Copyright (c) 2018, 2024 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ 'use strict'; const EventEmitter = require('events'); const assert = require('assert'); const HttpClient = require('./http_client'); const ops = require('./ops'); const TableState = require('./constants').TableState; const AdminState = require('./constants').AdminState; const ServiceType = require('./constants').ServiceType; const ErrorCode = require('./error_code'); const NoSQLTimeoutError = require('./error').NoSQLTimeoutError; const NoSQLProtocolError = require('./error').NoSQLProtocolError; const NoSQLArgumentError = require('./error').NoSQLArgumentError; const Config = require('./config'); const sleep = require('./utils').sleep; const replaceObject = require('./utils').replaceObject; const PreparedStatement = require('./stmt').PreparedStatement; const QueryPlanExecutor = require('./query/common').QueryPlanExecutor; const NoSQLError = require('./error').NoSQLError; //Default timeout for admin list operations such as listNamespaces, listUsers, //listRoles. const DEF_ADMIN_LIST_TIMEOUT = 30000; //Default limit for table usage iterable. The value is set to 24 hours of //1-minute usage records. const DEF_TABLE_USAGE_LIMIT = 1440; class NoSQLClientImpl extends EventEmitter { constructor(config) { super(); this._config = Config.create(config); this._client = new HttpClient(this._config); //Forward all events from the _client. this.on('newListener', (event, listener) => { this._client.on(event, listener); }); this.on('removeListener', (event, listener) => { this._client.removeListener(event, listener); }); //To prevent throwing if no error events are registered. //We should log the error in this listener. this.on('error', () => {}); } _assignOpt(opt, addOpt) { if (opt == null) { return addOpt; } //We don't throw here because we want the error to be handled //asynchronously. If a user passes invalid opt which is not //an object, we just return it and pass it to asynchronous code //for validation. if (typeof opt !== 'object') { return opt; } return Object.assign({}, opt, addOpt); } //Used by _forTableState, _forLocalReplicaInit and _forAdminCompletion. _doPollOpDelay(req, poReq, startTime) { if (req.opt.timeout !== Infinity) { const remaining = startTime + req.opt.timeout - (Date.now() + req.opt.delay); //Fail if we are less than opt.delay ahead of timing out. if (remaining <= 0) { throw new NoSQLTimeoutError(req.opt.timeout, null, req); } //Make sure timeout of poll op request (poReq) does not go past //total poll timeout. if (poReq.opt.timeout > remaining) { poReq.opt.timeout = remaining; } } return sleep(req.opt.delay); } async _forTableState(table, tableState, opt, skipInit) { const req = { api: typeof table === 'string' ? this.forTableState : this.forCompletion, table, tableState, opt }; //If called from _withCompletion, the work below was already done on //the original request. In addition this check avoids throwing wrong //error if remaining timeout < pollDelay. Same applies to //_forAdminCompletion(). if (!skipInit) { ops.PollTableStateOp.applyDefaults(req, this._config); ops.PollTableStateOp.validate(req); } // Should be set by PollTableStateOp.applyDefaults() above or from // _withCompletion(). assert(req.opt != null); //For forCompletion(), when table is TableResult, return if already //reached desired state. if (req.api === this.forCompletion && table.tableState === tableState) { return table; } //Request for GetTableOp. const gtReq = { api: req.api, table, opt: { compartment: req.opt.compartment, timeout: Math.min(this._config.timeout, req.opt.timeout) } }; const startTime = Date.now(); for(;;) { try { const res = await this._execute(ops.GetTableOp, gtReq); if (res.tableState === req.tableState) { return res; } } catch(err) { if (err.errorCode === ErrorCode.TABLE_NOT_FOUND && req.tableState === TableState.DROPPED) { const res = { tableName: gtReq.tableName, tableState: TableState.DROPPED }; //In this case onResult was not called by _execute above. ops.PollTableStateOp.onResult(this, req, res); return res; } throw err; } await this._doPollOpDelay(req, gtReq, startTime); } } async _forLocalReplicaInit(table, opt) { const req = { api: this.forLocalReplicaInit, table, opt }; if (this._config.serviceType !== ServiceType.CLOUD) { throw NoSQLError.create(ErrorCode.OPERATION_NOT_SUPPORTED, `forLocalReplicaInit is not supported for service type \ ${this._config.serviceType} (requires Cloud Service)`, null, req); } ops.PollTableOp.applyDefaults(req, this._config); ops.PollTableOp.validate(req); assert(req.opt != null); //Request for GetTableOp. const gtReq = { api: req.api, table, opt: { compartment: req.opt.compartment, timeout: Math.min(this._config.timeout, req.opt.timeout) } }; const startTime = Date.now(); for(;;) { const res = await this._execute(ops.GetTableOp, gtReq); if (res.isLocalReplicaInitialized) { return res; } await this._doPollOpDelay(req, gtReq, startTime); } } async * _tableUsageIterable(tableName, opt) { if (opt == null) { opt = {}; } else if (typeof opt === 'object') { opt = Object.assign({}, opt); opt.startIndex = undefined; } if (opt.limit == null) { opt.limit = DEF_TABLE_USAGE_LIMIT; } let res; do { res = await this.getTableUsage(tableName, opt); if (!res.usageRecords.length) { break; } opt.startIndex = res.nextIndex; yield res; } while(res.usageRecords.length === opt.limit); } async _getIndex(tableName, indexName, opt) { const req = { api: this.getIndex, tableName, opt }; req.opt = this._assignOpt(opt, { indexName }); const res = await this._client.execute(ops.GetIndexesOp, req); if (res.length != 1) { throw new NoSQLProtocolError( `Unexpected number of index results: ${res.length}`, null, req); } return res[0]; } //Note: This function is only used if opt.all is set to true and is not //fully implemented yet to account for throttling errors. Reserved for //future use. async _deleteRangeAll(tableName, key, opt) { const req = { api: this.deleteRange, tableName, key, opt }; //Accumulate deletedCount and consumedCapacity const total = { deletedCount: 0, consumedCapacity: this._config.serviceType !== ServiceType.KVSTORE ? { readKB: 0, readUnits: 0, writeKB: 0, writeUnits: 0 } : null }; for(;;) { const res = await this._client.execute(ops.MultiDeleteOp, req); assert(res.deletedCount != null); assert(res.consumedCapacity != null || total.consumedCapacity == null); total.deletedCount += res.deletedCount; if (total.consumedCapacity) { for(let key in total.consumedCapacity) { total.consumedCapacity[key] += res.consumedCapacity[key]; } } if (!res.continuationKey) { res.deletedCount = total.deletedCount; if (total.consumedCapacity) { Object.assign(res.consumedCapacity, total.consumedCapacity); } return res; } opt.continuationKey = res.continuationKey; } } async _prepare(stmt, opt) { const res = await this._client.execute(ops.PrepareOp, { api: this.prepare, stmt, opt }); res.__proto__ = PreparedStatement.prototype; return res; } async _query(stmt, opt) { const req = { api: this.query, opt }; let ck = opt ? opt.continuationKey : null; if (ck && ck._prepStmt) { req.prepStmt = ck._prepStmt; } else if (typeof stmt !== 'string') { req.prepStmt = stmt; } else { req.stmt = stmt; } if (!req.prepStmt) { const res = await this._client.execute(ops.QueryOp, req); //We always read the prepared statement if the request does not //have it. assert(res._prepStmt); //Simple query may already have results, so we just return them. if (!res._prepStmt._queryPlan) { return res; } //Advanced query will have no results in this case, only the //prepared statement. To make it more intuitive for the user, we //execute the first prepared query call. req.prepStmt = res._prepStmt; req.stmt = undefined; ck = res.continuationKey; assert(ck); //see QueryOp.onResult() assert(req.opt); //see Op.applyDefaults() req.opt.continuationKey = ck; } //Advanced query. if (req.prepStmt._queryPlan) { let qpExec = ck ? ck._qpExec : null; if (!qpExec) { //First advanced query call, create plan executor. qpExec = new QueryPlanExecutor(this, req.prepStmt); } return qpExec.execute(req); } //Simple query. return this._client.execute(ops.QueryOp, req); } async * _queryIterable(stmt, opt) { if (opt == null) { opt = {}; } else if (typeof opt === 'object') { opt = Object.assign({}, opt); opt.continuationKey = undefined; } do { const res = await this.query(stmt, opt); opt.continuationKey = res.continuationKey; yield res; } while(opt.continuationKey); } _execute(op, req) { return this._client.execute(op, req); } async _adminStatus(req) { if (req.adminResult != null && req.adminResult.operationId == null) { //still validate options passed for correctness ops.AdminStatusOp.applyDefaults(req, this._config); ops.AdminStatusOp.validate(req); return req.adminResult; } return this._execute(ops.AdminStatusOp, req); } async _forAdminCompletion(adminResult, opt, skipInit) { const req = { api: this.forCompletion, adminResult, opt }; if (!skipInit) { ops.AdminPollOp.applyDefaults(req, this._config); ops.AdminPollOp.validate(req); } // Should be set by AdminPollOp.applyDefaults() above or from // _withCompletion(). assert(req.opt != null); if (adminResult.state === AdminState.COMPLETE) { return adminResult; } //Request for AdminStatusOp. const asReq = { api: req.api, adminResult, opt: { timeout: Math.min(this._config.timeout, req.opt.timeout) } }; const startTime = Date.now(); for(;;) { const res = await this._execute(ops.AdminStatusOp, asReq); if (res.state === AdminState.COMPLETE) { return res; } await this._doPollOpDelay(req, asReq, startTime); } } async _forCompletion(res, opt, skipInit) { let ret; if (res == null) { throw new NoSQLArgumentError( 'forCompletion: missing result object'); } if (res._forAdmin) { ret = await this._forAdminCompletion(res, opt, skipInit); } else { const isDropTable = typeof res._stmt === 'string' && res._stmt.match(/^\s*DROP\s+TABLE\s+/i); ret = await this._forTableState(res, isDropTable ? TableState.DROPPED : TableState.ACTIVE, opt, skipInit); } return replaceObject(res, ret); } async _withCompletion(op, req) { const startTime = Date.now(); const res = await this._execute(op, req); //This initialization would be done by _execute() above. assert(req.opt != null && req.opt.__proto__ === this._config); let timeOut = req.opt._ownsTimeout ? req.opt.timeout : this._config.tablePollTimeout; if (timeOut !== Infinity) { timeOut -= (Date.now() - startTime); } req.opt.timeout = Math.max(timeOut, 1); return this._forCompletion(res, req.opt, true); } async _adminListOp(opName, stmt, req) { const listOpt = { complete: true, timeout: DEF_ADMIN_LIST_TIMEOUT }; if (req.opt == null) { req.opt = listOpt; } else if (typeof req.opt === 'object') { //if typeof opt !== 'object', it will be passed to adminDDL //and throw during validation req.opt = Object.assign(listOpt, req.opt); } let res = await this.adminDDL(stmt, req.opt); if (res.output == null) { throw new NoSQLProtocolError(`Missing output for ${opName}`, null, req); } try { res = JSON.parse(res.output); } catch(err) { throw new NoSQLProtocolError(`Error parsing output for \ ${opName}`, null, req); } if (res == null || typeof res !== 'object') { throw new NoSQLProtocolError(`Invalid output for ${opName}`, null, req); } return res; } async _listNamespaces(opt) { const req = { //will be included in any thrown errors api: this.listNamespaces, opt }; const res = await this._adminListOp('listNamespaces', 'SHOW AS JSON NAMESPACES', req); if (res.namespaces == null) { return []; } if (!Array.isArray(res.namespaces) || res.namespaces.findIndex( el => typeof el !== 'string') !== -1) { throw new NoSQLProtocolError('Invalid namespaces array in \ the output for listNamespaces operation', null, req); } return res.namespaces; } async _listUsers(opt) { const req = { //will be included in any thrown errors api: this.listUsers, opt }; const res = await this._adminListOp('listUsers', 'SHOW AS JSON USERS', req); if (res.users == null) { return []; } if (!Array.isArray(res.users)) { throw new NoSQLProtocolError('Invalid users array in the output \ for listUsers operation', null, req); } return res.users.map(user => { if (user == null || typeof user !== 'object' || typeof user.id !== 'string' || typeof user.name !== 'string') { throw new NoSQLProtocolError('Invalid value in the users \ array in the output for listUsers operation', null, req); } return { id: user.id, name: user.name }; }); } async _listRoles(opt) { const req = { //will be included in any thrown errors api: this.listRoles, opt }; const res = await this._adminListOp('listRoles', 'SHOW AS JSON ROLES', req); if (res.roles == null) { return []; } if (!Array.isArray(res.roles)) { throw new NoSQLProtocolError('Invalid roles array in the output \ for listRoles operation', null, req); } return res.roles.map(role => { if (role == null || typeof role !== 'object' || typeof role.name !== 'string') { throw new NoSQLProtocolError('Invalid value in the roles \ array in the output for listRoles operation', null, req); } return role.name; }); } //used for testing get _serialVersion() { return this._client.serialVersion; } close() { this._client.shutdown(); return Promise.resolve(Config.destroy(this._config)); } async _precacheAuth() { await this._config.auth.provider.getAuthorization({ opt: { __proto__: this._config }, lastError: new NoSQLError(ErrorCode.INVALID_AUTHORIZATION), _op: ops.GetTableOp }); return this; } } module.exports = NoSQLClientImpl;