@google-cloud/spanner
Version:
Cloud Spanner Client Library for Node.js
1,185 lines • 54.5 kB
JavaScript
"use strict";
/*!
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Interval = exports.SpannerDate = exports.PGNumeric = exports.Numeric = exports.Struct = exports.Int = exports.Float = exports.Float32 = exports.protos = exports.v1 = exports.MutationSet = exports.MutationGroup = exports.Transaction = exports.Snapshot = exports.PartitionedDml = exports.Table = exports.SessionPool = exports.Session = exports.Backup = exports.Database = exports.InstanceConfig = exports.Instance = exports.Spanner = void 0;
const service_1 = require("./common-grpc/service");
const precise_date_1 = require("@google-cloud/precise-date");
const projectify_1 = require("@google-cloud/projectify");
const promisify_1 = require("@google-cloud/promisify");
const extend = require("extend");
const google_auth_library_1 = require("google-auth-library");
const path = require("path");
const streamEvents = require("stream-events");
const through = require("through2");
const codec_1 = require("./codec");
Object.defineProperty(exports, "Float32", { enumerable: true, get: function () { return codec_1.Float32; } });
Object.defineProperty(exports, "Float", { enumerable: true, get: function () { return codec_1.Float; } });
Object.defineProperty(exports, "Int", { enumerable: true, get: function () { return codec_1.Int; } });
Object.defineProperty(exports, "Numeric", { enumerable: true, get: function () { return codec_1.Numeric; } });
Object.defineProperty(exports, "PGNumeric", { enumerable: true, get: function () { return codec_1.PGNumeric; } });
Object.defineProperty(exports, "SpannerDate", { enumerable: true, get: function () { return codec_1.SpannerDate; } });
Object.defineProperty(exports, "Interval", { enumerable: true, get: function () { return codec_1.Interval; } });
Object.defineProperty(exports, "Struct", { enumerable: true, get: function () { return codec_1.Struct; } });
const api_1 = require("@opentelemetry/api");
const backup_1 = require("./backup");
Object.defineProperty(exports, "Backup", { enumerable: true, get: function () { return backup_1.Backup; } });
const database_1 = require("./database");
Object.defineProperty(exports, "Database", { enumerable: true, get: function () { return database_1.Database; } });
const instance_1 = require("./instance");
Object.defineProperty(exports, "Instance", { enumerable: true, get: function () { return instance_1.Instance; } });
const instance_config_1 = require("./instance-config");
Object.defineProperty(exports, "InstanceConfig", { enumerable: true, get: function () { return instance_config_1.InstanceConfig; } });
const google_gax_1 = require("google-gax");
const protos_1 = require("../protos/protos");
var IsolationLevel = protos_1.google.spanner.v1.TransactionOptions.IsolationLevel;
var ReadLockMode = protos_1.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
const common_1 = require("./common");
const session_1 = require("./session");
Object.defineProperty(exports, "Session", { enumerable: true, get: function () { return session_1.Session; } });
const session_pool_1 = require("./session-pool");
Object.defineProperty(exports, "SessionPool", { enumerable: true, get: function () { return session_pool_1.SessionPool; } });
const table_1 = require("./table");
Object.defineProperty(exports, "Table", { enumerable: true, get: function () { return table_1.Table; } });
const transaction_1 = require("./transaction");
Object.defineProperty(exports, "MutationGroup", { enumerable: true, get: function () { return transaction_1.MutationGroup; } });
Object.defineProperty(exports, "MutationSet", { enumerable: true, get: function () { return transaction_1.MutationSet; } });
Object.defineProperty(exports, "PartitionedDml", { enumerable: true, get: function () { return transaction_1.PartitionedDml; } });
Object.defineProperty(exports, "Snapshot", { enumerable: true, get: function () { return transaction_1.Snapshot; } });
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
const grpcGcpModule = require("grpc-gcp");
const grpcGcp = grpcGcpModule(google_gax_1.grpc);
const v1 = require("./v1");
exports.v1 = v1;
const instrument_1 = require("./instrument");
const request_id_header_1 = require("./request_id_header");
const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
const interceptor_1 = require("./metrics/interceptor");
const spanner_metrics_exporter_1 = require("./metrics/spanner-metrics-exporter");
const metrics_tracer_factory_1 = require("./metrics/metrics-tracer-factory");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const gcpApiConfig = require('./spanner_grpc_config.json');
/**
* Retrieves the universe domain.
*
* This function checks for a universe domain in the following order:
* 1. The `universeDomain` property within the provided spanner options.
* 2. The `universe_domain` property within the provided spanner options.
* 3. The `GOOGLE_CLOUD_UNIVERSE_DOMAIN` environment variable.
* 4. If none of the above properties will be set, it will fallback to `googleapis.com`.
*
* For consistency with the Auth client, if the `universe_domain` option or the
* `GOOGLE_CLOUD_UNIVERSE_DOMAIN` env variable is used, this function will also set the
* `universeDomain` property within the provided `SpannerOptions` object. This ensures the
* Spanner client's universe domain aligns with the universe configured for authentication.
*
* @param {SpannerOptions} options - The Spanner client options.
* @returns {string} The universe domain.
*/
function getUniverseDomain(options) {
const universeDomainEnvVar = typeof process === 'object' && typeof process.env === 'object'
? process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN']
: undefined;
const universeDomain = options?.universeDomain ??
options?.universe_domain ??
universeDomainEnvVar ??
'googleapis.com';
// if the options.universe_domain/GOOGLE_CLOUD_UNIVERSE_DOMAIN env variable is set,
// set its value to the Spanner `universeDomain` options
// to match it with the universe from Auth Client
if (!options?.universeDomain &&
(options?.universe_domain || process.env.GOOGLE_CLOUD_UNIVERSE_DOMAIN)) {
options.universeDomain = universeDomain;
}
return universeDomain;
}
/**
* [Cloud Spanner](https://cloud.google.com/spanner) is a highly scalable,
* transactional, managed, NewSQL database service. Cloud Spanner solves the
* need for a horizontally-scaling database with consistent global transaction
* and SQL semantics. With Cloud Spanner you don't need to choose between
* consistency and horizontal scaling — you get both.
*
* @class
*
* @see [Cloud Spanner Documentation](https://cloud.google.com/spanner/docs)
* @see [Cloud Spanner Concepts](https://cloud.google.com/spanner/docs/concepts)
*
* @example Install the client library with <a
* href="https://www.npmjs.com/">npm</a>:
* ```
* npm install --save @google-cloud/spanner
* ```
*
* @example Create a client that uses <a
* href="https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application">Application
* Default Credentials (ADC)</a>:
* ```
* const client = new Spanner();
* ```
*
* @example Create a client with <a
* href="https://cloud.google.com/docs/authentication/production#obtaining_and_providing_service_account_credentials_manually">explicit
* credentials</a>:
* ```
* const client = new Spanner({ projectId:
* 'your-project-id', keyFilename: '/path/to/keyfile.json'
* });
* ```
*
* @example <caption>include:samples/quickstart.js</caption>
* region_tag:spanner_quickstart
* Full quickstart example:
*
* @param {ClientConfig} [options] Configuration options.
*/
class Spanner extends service_1.GrpcService {
options;
auth;
clients_;
instances_;
instanceConfigs_;
projectIdReplaced_;
projectId_;
projectFormattedName_;
commonHeaders_;
routeToLeaderEnabled = true;
directedReadOptions;
defaultTransactionOptions;
_observabilityOptions;
_universeDomain;
_isInSecureCredentials;
static _isAFEServerTimingEnabled;
_nthClientId;
/**
* Placeholder used to auto populate a column with the commit timestamp.
* This can only be used for timestamp columns that have set the option
* "(allow_commit_timestamp=true)" in the schema.
*
* @type {string}
*/
static COMMIT_TIMESTAMP = 'spanner.commit_timestamp()';
static POSTGRESQL = protos_1.google.spanner.admin.database.v1.DatabaseDialect.POSTGRESQL;
static GOOGLE_STANDARD_SQL = protos_1.google.spanner.admin.database.v1.DatabaseDialect.GOOGLE_STANDARD_SQL;
/**
* Returns whether AFE (Application Frontend Extension) server timing is enabled.
*
* This method checks the value of the environment variable
* `SPANNER_DISABLE_AFE_SERVER_TIMING`. If the variable is explicitly set to the
* string `'true'`, then AFE server timing is considered disabled, and this method
* returns `false`. For all other values (including if the variable is unset),
* the method returns `true`.
*
* @returns {boolean} `true` if AFE server timing is enabled; otherwise, `false`.
*/
static isAFEServerTimingEnabled = () => {
if (this._isAFEServerTimingEnabled === undefined) {
this._isAFEServerTimingEnabled =
process.env['SPANNER_DISABLE_AFE_SERVER_TIMING'] !== 'true';
}
return this._isAFEServerTimingEnabled;
};
/** Resets the cached value (use in tests if env changes). */
static _resetAFEServerTimingForTest() {
this._isAFEServerTimingEnabled = undefined;
}
/**
* Gets the configured Spanner emulator host from an environment variable.
*/
static getSpannerEmulatorHost() {
const endpointWithPort = process.env.SPANNER_EMULATOR_HOST;
if (endpointWithPort) {
if (endpointWithPort.startsWith('http:') ||
endpointWithPort.startsWith('https:')) {
throw new google_gax_1.GoogleError('SPANNER_EMULATOR_HOST must not start with a protocol specification (http/https)');
}
const index = endpointWithPort.indexOf(':');
if (index > -1) {
const portName = endpointWithPort.substring(index + 1);
const port = +portName;
if (!port || port < 1 || port > 65535) {
throw new google_gax_1.GoogleError(`Invalid port number: ${portName}`);
}
return {
endpoint: endpointWithPort.substring(0, index),
port: +endpointWithPort.substring(index + 1),
};
}
return { endpoint: endpointWithPort };
}
return undefined;
}
constructor(options) {
const scopes = [];
const clientClasses = [
v1.DatabaseAdminClient,
v1.InstanceAdminClient,
v1.SpannerClient,
];
for (const clientClass of clientClasses) {
for (const scope of clientClass.scopes) {
if (scopes.indexOf(scope) === -1) {
scopes.push(scope);
}
}
}
options = Object.assign({
libName: 'gccl',
libVersion: require('../../package.json').version,
scopes,
// Add grpc keep alive setting
'grpc.keepalive_time_ms': 120000,
// Enable grpc-gcp support
'grpc.callInvocationTransformer': grpcGcp.gcpCallInvocationTransformer,
'grpc.channelFactoryOverride': grpcGcp.gcpChannelFactoryOverride,
'grpc.gcpApiConfig': grpcGcp.createGcpApiConfig(gcpApiConfig),
grpc: google_gax_1.grpc,
}, options || {});
const directedReadOptions = options.directedReadOptions
? options.directedReadOptions
: null;
delete options.directedReadOptions;
const defaultTransactionOptions = options.defaultTransactionOptions
? options.defaultTransactionOptions
: {
isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED,
};
delete options.defaultTransactionOptions;
if (options?.universe_domain &&
options?.universeDomain &&
options?.universe_domain !== options?.universeDomain) {
throw new Error('Please set either universe_domain or universeDomain, but not both.');
}
const emulatorHost = Spanner.getSpannerEmulatorHost();
if (emulatorHost &&
emulatorHost.endpoint &&
emulatorHost.endpoint.length > 0) {
options.servicePath = emulatorHost.endpoint;
options.port = emulatorHost.port;
options.sslCreds = google_gax_1.grpc.credentials.createInsecure();
}
const universeEndpoint = getUniverseDomain(options);
const spannerUniverseEndpoint = 'spanner.' + universeEndpoint;
const config = {
baseUrl: options.apiEndpoint || options.servicePath || spannerUniverseEndpoint,
protosDir: path.resolve(__dirname, '../protos'),
protoServices: {
Operations: {
path: 'google/longrunning/operations.proto',
service: 'longrunning',
},
},
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
packageJson: require('../../package.json'),
};
super(config, options);
if (options.routeToLeaderEnabled === false) {
this.routeToLeaderEnabled = false;
}
this._isInSecureCredentials = options.sslCreds?._isSecure() === false;
this.options = options;
this.auth = new google_auth_library_1.GoogleAuth(this.options);
this.clients_ = new Map();
this.instances_ = new Map();
this.instanceConfigs_ = new Map();
this.projectIdReplaced_ = false;
this.projectFormattedName_ = 'projects/' + this.projectId;
this.directedReadOptions = directedReadOptions;
this.defaultTransactionOptions = defaultTransactionOptions;
this._observabilityOptions = options.observabilityOptions;
this.commonHeaders_ = (0, common_1.getCommonHeaders)(this.projectFormattedName_, this._observabilityOptions?.enableEndToEndTracing);
(0, instrument_1.ensureInitialContextManagerSet)();
this._nthClientId = (0, request_id_header_1.nextSpannerClientId)();
this._universeDomain = universeEndpoint;
this.projectId_ = options.projectId;
this.configureMetrics_(options.disableBuiltInMetrics);
}
get universeDomain() {
return this._universeDomain;
}
/**
* Gets the InstanceAdminClient object.
* The returned InstanceAdminClient object is a shared, managed instance and should not be manually closed.
* @returns {v1.InstanceAdminClient} The InstanceAdminClient object
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner({
* projectId: projectId,
* });
* const instanceAdminClient = spanner.getInstanceAdminClient();
* ```
*/
getInstanceAdminClient() {
const clientName = 'InstanceAdminClient';
if (!this.clients_.has(clientName)) {
this.clients_.set(clientName, new v1[clientName](this.options));
}
return this.clients_.get(clientName);
}
/**
* Gets the DatabaseAdminClient object.
* The returned DatabaseAdminClient object is a managed, shared instance and should not be manually closed.
* @returns {v1.DatabaseAdminClient} The DatabaseAdminClient object.
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner({
* projectId: projectId,
* });
* const databaseAdminClient = spanner.getDatabaseAdminClient();
* ```
*/
getDatabaseAdminClient() {
const clientName = 'DatabaseAdminClient';
if (!this.clients_.has(clientName)) {
this.clients_.set(clientName, new v1[clientName](this.options));
}
return this.clients_.get(clientName);
}
/** Closes this Spanner client and cleans up all resources used by it. */
close() {
this.clients_.forEach(c => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = c;
if (client.operationsClient && client.operationsClient.close) {
client.operationsClient.close();
}
client.close();
});
cleanup().catch(err => {
console.error('Error occured during cleanup: ', err);
});
}
createInstance(name, config, callback) {
if (!name) {
throw new google_gax_1.GoogleError('A name is required to create an instance.');
}
if (!config) {
throw new google_gax_1.GoogleError(['A configuration object is required to create an instance.'].join(''));
}
const formattedName = instance_1.Instance.formatName_(this.projectId, name);
const displayName = config.displayName || formattedName.split('/').pop();
const reqOpts = {
parent: this.projectFormattedName_,
instanceId: formattedName.split('/').pop(),
instance: extend({
name: formattedName,
displayName,
nodeCount: config.nodes,
processingUnits: config.processingUnits,
}, config),
};
if (reqOpts.instance.nodeCount && reqOpts.instance.processingUnits) {
throw new google_gax_1.GoogleError(['Only one of nodeCount or processingUnits can be specified.'].join(''));
}
if (!reqOpts.instance.nodeCount && !reqOpts.instance.processingUnits) {
// If neither nodes nor processingUnits are specified, default to a
// nodeCount of 1.
reqOpts.instance.nodeCount = 1;
}
delete reqOpts.instance.nodes;
delete reqOpts.instance.gaxOptions;
if (config.config.indexOf('/') === -1) {
reqOpts.instance.config = `projects/${this.projectId}/instanceConfigs/${config.config}`;
}
this.request({
client: 'InstanceAdminClient',
method: 'createInstance',
reqOpts,
gaxOpts: config.gaxOptions,
headers: this.commonHeaders_,
}, (err, operation, resp) => {
if (err) {
callback(err, null, null, resp);
return;
}
const instance = this.instance(formattedName);
instance._observabilityOptions = this._observabilityOptions;
callback(null, instance, operation, resp);
});
}
getInstances(optionsOrCallback, cb) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
parent: 'projects/' + this.projectId,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageToken;
delete gaxOpts.pageSize;
}
this.request({
client: 'InstanceAdminClient',
method: 'listInstances',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, instances, nextPageRequest, ...args) => {
let instanceInstances = null;
if (instances) {
instanceInstances = instances.map(instance => {
const instanceInstance = self.instance(instance.name);
instanceInstance.metadata = instance;
return instanceInstance;
});
}
const nextQuery = nextPageRequest
? extend({}, options, nextPageRequest)
: null;
callback(err, instanceInstances, nextQuery, ...args);
});
}
/**
* Get a list of {@link Instance} objects as a readable object stream.
*
* Wrapper around {@link v1.InstanceAdminClient#listInstances}.
*
* @see {@link v1.InstanceAdminClient#listInstances}
* @see [ListInstances API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.InstanceAdmin.ListInstances)
*
* @method Spanner#getInstancesStream
* @param {GetInstancesOptions} [options] Query object for listing instances.
* @returns {ReadableStream} A readable stream that emits {@link Instance}
* instances.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* spanner.getInstancesStream()
* .on('error', console.error)
* .on('data', function(instance) {
* // `instance` is an `Instance` object.
* })
* .on('end', function() {
* // All instances retrieved.
* });
*
* //-
* // If you anticipate many results, you can end a stream early to prevent
* // unnecessary processing and API requests.
* //-
* spanner.getInstancesStream()
* .on('data', function(instance) {
* this.end();
* });
* ```
*/
getInstancesStream(options = {}) {
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
parent: 'projects/' + this.projectId,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
return this.requestStream({
client: 'InstanceAdminClient',
method: 'listInstancesStream',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
});
}
createInstanceConfig(name, config, callback) {
if (!name) {
throw new google_gax_1.GoogleError('A name is required to create an instance config.');
}
if (!config) {
throw new google_gax_1.GoogleError([
'A configuration object is required to create an instance config.',
].join(''));
}
if (!config.baseConfig) {
throw new google_gax_1.GoogleError(['Base instance config is required to create an instance config.'].join(''));
}
const formattedName = instance_config_1.InstanceConfig.formatName_(this.projectId, name);
const displayName = config.displayName || formattedName.split('/').pop();
const reqOpts = {
parent: this.projectFormattedName_,
instanceConfigId: formattedName.split('/').pop(),
instanceConfig: extend({
name: formattedName,
displayName,
}, config),
validateOnly: config.validateOnly,
};
if (config.baseConfig.indexOf('/') === -1) {
reqOpts.instanceConfig.baseConfig = `projects/${this.projectId}/instanceConfigs/${config.baseConfig}`;
}
// validateOnly need not be passed in if it is null.
if (reqOpts.validateOnly === null || reqOpts.validateOnly === undefined)
delete reqOpts.validateOnly;
// validateOnly and gaxOptions are not fields in InstanceConfig.
delete reqOpts.instanceConfig.validateOnly;
delete reqOpts.instanceConfig.gaxOptions;
this.request({
client: 'InstanceAdminClient',
method: 'createInstanceConfig',
reqOpts,
gaxOpts: config.gaxOptions,
headers: this.commonHeaders_,
}, (err, operation, resp) => {
if (err) {
callback(err, null, null, resp);
return;
}
const instanceConfig = this.instanceConfig(formattedName);
callback(null, instanceConfig, operation, resp);
});
}
getInstanceConfigs(optionsOrCallback, cb) {
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
parent: 'projects/' + this.projectId,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
return this.request({
client: 'InstanceAdminClient',
method: 'listInstanceConfigs',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, instanceConfigs, nextPageRequest, ...args) => {
const nextQuery = nextPageRequest
? extend({}, options, nextPageRequest)
: null;
callback(err, instanceConfigs, nextQuery, ...args);
});
}
/**
* Get a list of instance configs as a readable object stream.
*
* Wrapper around {@link v1.InstanceAdminClient#listInstanceConfigsStream}.
*
* @see {@link v1.InstanceAdminClient#listInstanceConfigsStream}
* @see [ListInstanceConfigs API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs)
*
* @method Spanner#getInstanceConfigsStream
* @param {GetInstanceConfigsOptions} [options] Query object for listing instance
* configs.
* @returns {ReadableStream} A readable stream that emits instance configs.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* spanner.getInstanceConfigsStream()
* .on('error', console.error)
* .on('data', function(instanceConfig) {})
* .on('end', function() {
* // All instances retrieved.
* });
*
* //-
* // If you anticipate many results, you can end a stream early to prevent
* // unnecessary processing and API requests.
* //-
* spanner.getInstanceConfigsStream()
* .on('data', function(instanceConfig) {
* this.end();
* });
* ```
*/
getInstanceConfigsStream(options = {}) {
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
parent: 'projects/' + this.projectId,
});
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
delete reqOpts.gaxOptions;
return this.requestStream({
client: 'InstanceAdminClient',
method: 'listInstanceConfigsStream',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
});
}
getInstanceConfig(name, optionsOrCallback, cb) {
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const reqOpts = extend({}, {
name: 'projects/' + this.projectId + '/instanceConfigs/' + name,
});
const gaxOpts = extend({}, options.gaxOptions);
return this.request({
client: 'InstanceAdminClient',
method: 'getInstanceConfig',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, instanceConfig) => {
callback(err, instanceConfig);
});
}
getInstanceConfigOperations(optionsOrCallback, cb) {
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
parent: this.projectFormattedName_,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However, values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
this.request({
client: 'InstanceAdminClient',
method: 'listInstanceConfigOperations',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, operations, nextPageRequest, ...args) => {
const nextQuery = nextPageRequest
? extend({}, options, nextPageRequest)
: null;
callback(err, operations, nextQuery, ...args);
});
}
/**
* Get a reference to an Instance object.
*
* @throws {GoogleError} If a name is not provided.
*
* @param {string} name The name of the instance.
* @returns {Instance} An Instance object.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
* const instance = spanner.instance('my-instance');
* ```
*/
instance(name) {
if (!name) {
throw new google_gax_1.GoogleError('A name is required to access an Instance object.');
}
const key = name.split('/').pop();
if (!this.instances_.has(key)) {
this.instances_.set(key, new instance_1.Instance(this, name));
}
return this.instances_.get(key);
}
/**
* Get a reference to an InstanceConfig object.
*
* @throws {GoogleError} If a name is not provided.
*
* @param {string} name The name of the instance config.
* @returns {InstanceConfig} An InstanceConfig object.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
* const instanceConfig = spanner.instanceConfig('my-instance-config');
* ```
*/
instanceConfig(name) {
if (!name) {
throw new google_gax_1.GoogleError('A name is required to access an InstanceConfig object.');
}
const key = name.split('/').pop();
if (!this.instanceConfigs_.has(key)) {
this.instanceConfigs_.set(key, new instance_config_1.InstanceConfig(this, name));
}
return this.instanceConfigs_.get(key);
}
/**
* Setup the OpenTelemetry metrics capturing for service metrics to Google Cloud Monitoring.
*/
configureMetrics_(disableBuiltInMetrics) {
// Only enable metrics if not explicitly disabled and we are not using
// insecure credentials.
const metricsEnabled = process.env.SPANNER_DISABLE_BUILTIN_METRICS !== 'true' &&
!disableBuiltInMetrics &&
!this._isInSecureCredentials;
if (metricsEnabled) {
try {
this.auth.getProjectId((err, projectId) => {
if (err || !projectId) {
console.error('Unable to get Project Id for client side metrics, will skip exporting client' +
' side metrics' +
err);
return;
}
metrics_tracer_factory_1.MetricsTracerFactory.enabled = metricsEnabled;
this.projectId_ = projectId;
const factory = metrics_tracer_factory_1.MetricsTracerFactory.getInstance(projectId);
const periodicReader = new sdk_metrics_1.PeriodicExportingMetricReader({
exporter: new spanner_metrics_exporter_1.CloudMonitoringMetricsExporter({ auth: this.auth }, projectId),
exportIntervalMillis: 60000,
});
// Retrieve the MeterProvider to trigger construction
factory.getMeterProvider([periodicReader]);
});
}
catch (err) {
console.error('Unable to configure client side metrics, will skip exporting client' +
' side metrics' +
err);
}
}
}
/**
* Prepare a gapic request. This will cache the GAX client and replace
* {{projectId}} placeholders, if necessary.
*
* @private
*
* @param {object} config Request config
* @param {function} callback Callback function
*/
prepareGapicRequest_(config, callback) {
this.auth.getProjectId((err, projectId) => {
if (err) {
callback(err);
return;
}
const clientName = config.client;
try {
if (!this.clients_.has(clientName)) {
this.clients_.set(clientName, new v1[clientName](this.options));
}
}
catch (err) {
callback(err, null);
}
const gaxClient = this.clients_.get(clientName);
let reqOpts = extend(true, {}, config.reqOpts);
reqOpts = (0, projectify_1.replaceProjectIdToken)(reqOpts, projectId);
// It would have been preferable to replace the projectId already in the
// constructor of Spanner, but that is not possible as auth.getProjectId
// is an async method. This is therefore the first place where we have
// access to the value that should be used instead of the placeholder.
if (!this.projectIdReplaced_) {
this.projectId = (0, projectify_1.replaceProjectIdToken)(this.projectId, projectId);
this.projectFormattedName_ = (0, projectify_1.replaceProjectIdToken)(this.projectFormattedName_, projectId);
this.instances_.forEach(instance => {
instance.formattedName_ = (0, projectify_1.replaceProjectIdToken)(instance.formattedName_, projectId);
instance.databases_.forEach(database => {
database.formattedName_ = (0, projectify_1.replaceProjectIdToken)(database.formattedName_, projectId);
});
});
this.projectIdReplaced_ = true;
}
config.headers[common_1.CLOUD_RESOURCE_HEADER] = (0, projectify_1.replaceProjectIdToken)(config.headers[common_1.CLOUD_RESOURCE_HEADER], projectId);
// Do context propagation
api_1.propagation.inject(api_1.context.active(), config.headers, {
set: (carrier, key, value) => {
carrier[key] = value; // Set the span context (trace and span ID)
},
});
// Attach the x-goog-spanner-request-id to the currently active span.
(0, request_id_header_1.attributeXGoogSpannerRequestIdToActiveSpan)(config);
const interceptors = [];
if (metrics_tracer_factory_1.MetricsTracerFactory.enabled) {
interceptors.push(interceptor_1.MetricInterceptor);
}
const requestFn = gaxClient[config.method].bind(gaxClient, reqOpts,
// Add headers to `gaxOpts`
extend(true, {}, config.gaxOpts, {
otherArgs: {
headers: config.headers,
options: {
interceptors: interceptors,
},
},
}));
// Wrap requestFn to inject the spanner request id into every returned error.
const wrappedRequestFn = (...args) => {
const hasCallback = args &&
args.length > 0 &&
typeof args[args.length - 1] === 'function';
switch (hasCallback) {
case true: {
const cb = args[args.length - 1];
const priorArgs = args.slice(0, args.length - 1);
requestFn(...priorArgs, (...results) => {
if (results && results.length > 0) {
const err = results[0];
(0, request_id_header_1.injectRequestIDIntoError)(config, err);
}
cb(...results);
});
return;
}
case false: {
const res = requestFn(...args);
const stream = res;
if (stream) {
stream.on('error', err => {
(0, request_id_header_1.injectRequestIDIntoError)(config, err);
});
}
const originallyPromise = res instanceof Promise;
if (!originallyPromise) {
return res;
}
return new Promise((resolve, reject) => {
requestFn(...args)
.then(resolve)
.catch(err => {
(0, request_id_header_1.injectRequestIDIntoError)(config, err);
reject(err);
});
});
}
}
};
callback(null, wrappedRequestFn);
});
}
/**
* Funnel all API requests through this method to be sure we have a project
* ID.
*
* @param {object} config Configuration object.
* @param {object} config.gaxOpts GAX options.
* @param {function} config.method The gax method to call.
* @param {object} config.reqOpts Request options.
* @param {function} [callback] Callback function.
* @returns {Promise}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request(config, callback) {
let metricsTracer = null;
if (config.client === 'SpannerClient' && this.projectId_) {
metricsTracer =
metrics_tracer_factory_1.MetricsTracerFactory?.getInstance(this.projectId_)?.createMetricsTracer(config.method, config.reqOpts.database ?? config.reqOpts.session, config.headers['x-goog-spanner-request-id']) ?? null;
}
metricsTracer?.recordOperationStart();
if (typeof callback === 'function') {
this.prepareGapicRequest_(config, (err, requestFn) => {
if (err) {
callback(err);
metricsTracer?.recordOperationCompletion();
}
else {
const wrappedCallback = (...args) => {
metricsTracer?.recordOperationCompletion();
callback(...args);
};
requestFn(wrappedCallback);
}
});
}
else {
return new Promise((resolve, reject) => {
this.prepareGapicRequest_(config, (err, requestFn) => {
if (err) {
metricsTracer?.recordOperationCompletion();
reject(err);
}
else {
const result = requestFn();
if (result && typeof result.then === 'function') {
result
.then(val => {
metricsTracer?.recordOperationCompletion();
resolve(val);
})
.catch(error => {
metricsTracer?.recordOperationCompletion();
reject(error);
});
}
else {
metricsTracer?.recordOperationCompletion();
resolve(result);
}
}
});
});
}
}
/**
* Funnel all streaming API requests through this method to be sure we have a
* project ID.
*
* @param {object} config Configuration object.
* @param {object} config.gaxOpts GAX options.
* @param {function} config.method The gax method to call.
* @param {object} config.reqOpts Request options.
* @param {function} [callback] Callback function.
* @returns {Stream}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestStream(config) {
let metricsTracer = null;
if (config.client === 'SpannerClient' && this.projectId_) {
metricsTracer =
metrics_tracer_factory_1.MetricsTracerFactory?.getInstance(this.projectId_)?.createMetricsTracer(config.method, config.reqOpts.session ?? config.reqOpts.database, config.headers['x-goog-spanner-request-id']) ?? null;
}
metricsTracer?.recordOperationStart();
const stream = streamEvents(through.obj());
stream.once('reading', () => {
this.prepareGapicRequest_(config, (err, requestFn) => {
if (err) {
stream.destroy(err);
return;
}
requestFn()
.on('error', err => {
stream.destroy(err);
})
.pipe(stream);
});
});
stream.on('finish', () => {
stream.destroy();
});
stream.on('close', () => {
metricsTracer?.recordOperationCompletion();
});
return stream;
}
/**
* Helper function to get a Cloud Spanner Date object.
*
* DATE types represent a logical calendar date, independent of time zone.
* DATE values do not represent a specific 24-hour period. Rather, a given
* DATE value represents a different 24-hour period when interpreted in a
* different time zone. Because of this, all values passed to
* {@link Spanner.date} will be interpreted as local time.
*
* To represent an absolute point in time, use {@link Spanner.timestamp}.
*
* @param {string|number} [date] String representing the date or number
* representing the year.
* @param {number} [month] Number representing the month.
* @param {number} [date] Number representing the date.
* @returns {SpannerDate}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const date = Spanner.date('08-20-1969');
* ```
*/
static date(dateStringOrYear, month, date) {
if (typeof dateStringOrYear === 'number') {
return new codec_1.codec.SpannerDate(dateStringOrYear, month, date);
}
return new codec_1.codec.SpannerDate(dateStringOrYear);
}
/**
* Date object with nanosecond precision. Supports all standard Date arguments
* in addition to several custom types.
* @external PreciseDate
* @see {@link https://github.com/googleapis/nodejs-precise-date|PreciseDate}
*/
/**
* Helper function to get a Cloud Spanner Timestamp object.
*
* String timestamps should have a canonical format of
* `YYYY-[M]M-[D]D[( |T)[H]H:[M]M:[S]S[.DDDDDDDDD]]Z`
*
* **Timestamp values must be expressed in Zulu time and cannot include a UTC
* offset.**
*
* @see https://cloud.google.com/spanner/docs/data-types#timestamp-type
*
* @param {string|number|google.protobuf.Timestamp|external:PreciseDate}
* [timestamp] Either a RFC 3339 timestamp formatted string or a
* {@link google.protobuf.Timestamp} object. If a PreciseDate is given, it
* will return that timestamp as is.
* @returns {external:PreciseDate}
*
* @example
* ```
* const timestamp = Spanner.timestamp('2019-02-08T10:34:29.481145231Z');
*
* ```
* @example With a `google.protobuf.Timestamp` object
* ```
* const [seconds, nanos] = process.hrtime();
* const timestamp = Spanner.timestamp({seconds, nanos});
* ```
*
* @example With a Date timestamp
* ```
* const timestamp = Spanner.timestamp(Date.now());
* ```
*/
static timestamp(value) {
value = value || Date.now();
if (value instanceof precise_date_1.PreciseDate) {
return value;
}
return new precise_date_1.PreciseDate(value);
}
/**
* Helper function to get a Cloud Spanner Float32 object.
*
* @param {string|number} value The float as a number or string.
* @returns {Float32}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const float = Spanner.float32(10);
* ```
*/
static float32(value) {
return new codec_1.codec.Float32(value);
}
/**
* Helper function to get a Cloud Spanner Float64 object.
*
* @param {string|number} value The float as a number or string.
* @returns {Float}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const float = Spanner.float(10);
* ```
*/
static float(value) {
return new codec_1.codec.Float(value);
}
/**
* Helper function to get a Cloud Spanner Int64 object.
*
* @param {string|number} value The int as a number or string.
* @returns {Int}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const int = Spanner.int(10);
* ```
*/
static int(value) {
return new codec_1.codec.Int(value);
}
/**
* Helper function to get a Cloud Spanner Numeric object.
*
* @param {string} value The numeric value as a string.
* @returns {Numeric}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const numeric = Spanner.numeric("3.141592653");
* ```
*/
static numeric(value) {
return new codec_1.codec.Numeric(value);
}
/**
* Helper function to get a Cloud Spanner pgNumeric object.
*
* @param {string} value The pgNumeric value as a string.
* @returns {PGNumeric}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const pgNumeric = Spanner.pgNumeric("3.141592653");
* ```
*/
static pgNumeric(value) {
return new codec_1.codec.PGNumeric(value);
}
/**
* Helper function to get a Cloud Spanner pgJsonb object.
*
* @param {object|string} value The pgJsonb value as a string or object.
* @returns {PGJsonb}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const pgJsonb1 = Spanner.pgJsonb({rating: 6});
* const pgJsonb2 = Spanner.pgJsonb(`[
* {
* "name": null,
* "open": true
* }]`)
* ```
*/
static pgJsonb(value) {
return new codec_1.codec.PGJsonb(value);
}
/**
* Helper function to get a Cloud Spanner Interval object.
*
* @param {number} months The months part of Interval as number.
* @param {number} days The days part of Interval as number.
* @param {bigint} n