Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions handwritten/spanner/src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
* @param value The value to convert into an array.
* @returns An array containing the value, or an empty array.
*/
export function toArray(value: any) {

Check warning on line 101 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (value === null || value === undefined) {
return [];
}
Expand All @@ -123,7 +123,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is NOT `undefined`, otherwise `false`.
*/
export function isDefined(value: any): boolean {

Check warning on line 126 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return typeof value !== 'undefined';
}

Expand All @@ -132,7 +132,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is null, otherwise `false`.
*/
export function isNull(value: any): boolean {

Check warning on line 135 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return value === null;
}

Expand All @@ -141,7 +141,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is `undefined`, otherwise `false`.
*/
export function isUndefined(value: any): boolean {

Check warning on line 144 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return typeof value === 'undefined';
}

Expand All @@ -150,7 +150,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is empty, otherwise `false`.
*/
export function isEmpty(value: any): boolean {

Check warning on line 153 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const type = Object.prototype.toString.call(value);
if (
type === '[object Array]' ||
Expand All @@ -175,7 +175,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is an object, otherwise `false`.
*/
export function isObject(value: any): boolean {

Check warning on line 178 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return Object.prototype.toString.call(value) === '[object Object]';
}

Expand All @@ -184,7 +184,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is string, otherwise `false`.
*/
export function isString(value: any): boolean {

Check warning on line 187 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return Object.prototype.toString.call(value) === '[object String]';
}

Expand All @@ -193,7 +193,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is an array, otherwise `false`.
*/
export function isArray(value: any): boolean {

Check warning on line 196 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return Array.isArray(value);
}

Expand All @@ -202,7 +202,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is a `Date` object, otherwise `false`.
*/
export function isDate(value: any): boolean {

Check warning on line 205 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return Object.prototype.toString.call(value) === '[object Date]';
}

Expand All @@ -211,7 +211,7 @@
* @param {*} value The value to check.
* @returns {Boolean} `true` if the value is boolean, otherwise `false`.
*/
export function isBoolean(value: any): boolean {

Check warning on line 214 in handwritten/spanner/src/helper.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return Object.prototype.toString.call(value) === '[object Boolean]';
}

Expand Down Expand Up @@ -365,6 +365,51 @@
return value;
}

/**
* Checks whether an input value contains the `{{projectId}}` placeholder.
*
* @param {*} value - The value to inspect.
* @return {boolean} - `true` if any placeholder is found, otherwise `false`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function hasProjectIdToken(value: any): boolean {
if (typeof value === 'string') {
return value.includes(PROJECT_ID_TOKEN);
}

if (
value === null ||
typeof value !== 'object' ||
value instanceof Buffer ||
value instanceof Stream ||
isDate(value)
) {
return false;
}

if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
if (hasProjectIdToken(value[i])) {
return true;
}
}
return false;
}

for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
if (!KEYS_TO_SCAN.has(key)) {
continue;
}
if (hasProjectIdToken(value[key])) {
return true;
}
}
}

return false;
}

/**
* Custom error type for missing project ID errors.
*/
Expand Down
230 changes: 167 additions & 63 deletions handwritten/spanner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import {GrpcService, GrpcServiceConfig} from './common-grpc/service';
import {PreciseDate} from '@google-cloud/precise-date';
import {replaceProjectIdToken} from './helper';
import {hasProjectIdToken, replaceProjectIdToken} from './helper';
import {promisifyAll} from '@google-cloud/promisify';
import * as extend from 'extend';
import {GoogleAuth, GoogleAuthOptions} from 'google-auth-library';
Expand Down Expand Up @@ -331,6 +331,9 @@ class Spanner extends GrpcService {
private _metricsEnabled = false;
private static _isAFEServerTimingEnabled: boolean | undefined;
readonly _nthClientId: number;
private _pendingProjectIdCallbacks?: Array<
(error: Error | null, projectId?: string) => void
>;

/**
* Placeholder used to auto populate a column with the commit timestamp.
Expand Down Expand Up @@ -534,7 +537,7 @@ class Spanner extends GrpcService {
if (!this.clients_.has(clientName)) {
this.clients_.set(
clientName,
new v1[clientName](this.options as ClientOptions),
new v1.InstanceAdminClient(this.options as ClientOptions),
);
}
return this.clients_.get(clientName)! as v1.InstanceAdminClient;
Expand All @@ -558,7 +561,7 @@ class Spanner extends GrpcService {
if (!this.clients_.has(clientName)) {
this.clients_.set(
clientName,
new v1[clientName](this.options as ClientOptions),
new v1.DatabaseAdminClient(this.options as ClientOptions),
);
}
return this.clients_.get(clientName)! as v1.DatabaseAdminClient;
Expand Down Expand Up @@ -615,10 +618,12 @@ class Spanner extends GrpcService {

if (callback) {
// process.nextTick prevents Unhandled Promise Rejections if callback throws
res.then(
() => process.nextTick(() => callback(null)),
err => process.nextTick(() => callback(err)),
);
res
.then(
() => process.nextTick(() => callback(null)),
err => process.nextTick(() => callback(err)),
)
.catch(() => {});
} else {
return res;
}
Expand Down Expand Up @@ -1718,51 +1723,136 @@ class Spanner extends GrpcService {
* @param {object} config Request config
* @param {function} callback Callback function
*/
prepareGapicRequest_(config, callback) {
this.auth.getProjectId((err, projectId) => {
if (err) {
callback(err);
prepareGapicRequest_(
config: RequestConfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (err: Error | null, requestFn?: any) => void,
): void {
if (this.projectId && this.projectIdReplaced_) {
this._prepareGapicRequestWithProjectId(config, this.projectId, callback);
return;
}
Comment thread
olavloite marked this conversation as resolved.
if (this._pendingProjectIdCallbacks) {
this._pendingProjectIdCallbacks.push((error, projectId) => {
if (error) {
callback(error);
return;
}
this._prepareGapicRequestWithProjectId(config, projectId!, callback);
});
return;
}
this._pendingProjectIdCallbacks = [];
this.auth.getProjectId((error, projectId) => {
const pendingCallbacks = this._pendingProjectIdCallbacks || [];
this._pendingProjectIdCallbacks = undefined;
if (error) {
try {
callback(error);
} finally {
for (const pendingCallback of pendingCallbacks) {
try {
pendingCallback(error);
} catch {
// Prevent one failing user callback from stranding subsequent pending callers.
}
}
}
return;
}
const clientName = config.client;
try {
if (!this.clients_.has(clientName)) {
this.clients_.set(clientName, new v1[clientName](this.options));
this._prepareGapicRequestWithProjectId(config, projectId!, callback);
} finally {
for (const pendingCallback of pendingCallbacks) {
try {
pendingCallback(null, projectId!);
} catch {
// Prevent one failing user callback from stranding subsequent pending callers.
}
}
} catch (err) {
callback(err, null);
}
});
}

private _prepareGapicRequestWithProjectId(
config: RequestConfig,
projectId: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (err: Error | null, requestFn?: any) => void,
): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let wrappedRequestFn: any;
try {
Comment thread
olavloite marked this conversation as resolved.
const clientName = config.client;
if (!this.clients_.has(clientName)) {
// eslint-disable-next-line import/namespace
this.clients_.set(clientName, new v1[clientName](this.options));
}
const gaxClient = this.clients_.get(clientName)!;
let reqOpts = extend(true, {}, config.reqOpts);
reqOpts = 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.
let reqOpts = config.reqOpts;
if (!this.projectIdReplaced_ || hasProjectIdToken(reqOpts)) {
reqOpts = extend(true, {}, config.reqOpts);
reqOpts = replaceProjectIdToken(reqOpts, projectId);
}
if (!this.projectIdReplaced_) {
this.projectId = replaceProjectIdToken(this.projectId, projectId!);
this.projectId = replaceProjectIdToken(this.projectId, projectId);
this.projectFormattedName_ = replaceProjectIdToken(
this.projectFormattedName_,
projectId!,
projectId,
);
if (
this.commonHeaders_[CLOUD_RESOURCE_HEADER]?.includes('{{projectId}}')
) {
this.commonHeaders_[CLOUD_RESOURCE_HEADER] = replaceProjectIdToken(
this.commonHeaders_[CLOUD_RESOURCE_HEADER],
projectId,
);
}
this.instances_.forEach(instance => {
instance.formattedName_ = replaceProjectIdToken(
instance.formattedName_,
projectId!,
projectId,
);
if (
instance.commonHeaders_?.[CLOUD_RESOURCE_HEADER]?.includes(
'{{projectId}}',
)
) {
instance.commonHeaders_[CLOUD_RESOURCE_HEADER] =
replaceProjectIdToken(
instance.commonHeaders_[CLOUD_RESOURCE_HEADER],
projectId,
);
}
instance.databases_.forEach(database => {
database.formattedName_ = replaceProjectIdToken(
database.formattedName_,
projectId!,
projectId,
);
if (
database.commonHeaders_?.[CLOUD_RESOURCE_HEADER]?.includes(
'{{projectId}}',
)
) {
database.commonHeaders_[CLOUD_RESOURCE_HEADER] =
replaceProjectIdToken(
database.commonHeaders_[CLOUD_RESOURCE_HEADER],
projectId,
);
}
});
});
this.projectIdReplaced_ = true;
}
config.headers[CLOUD_RESOURCE_HEADER] = replaceProjectIdToken(
config.headers[CLOUD_RESOURCE_HEADER],
projectId!,
);
if (!config.headers) {
config.headers = {};
}
if (config.headers[CLOUD_RESOURCE_HEADER]?.includes('{{projectId}}')) {
config.headers[CLOUD_RESOURCE_HEADER] = replaceProjectIdToken(
config.headers[CLOUD_RESOURCE_HEADER],
projectId,
);
}
if (isTracingEnabled(this._observabilityOptions)) {
// Do context propagation
propagation.inject(context.active(), config.headers, {
Expand All @@ -1773,26 +1863,34 @@ class Spanner extends GrpcService {
// Attach the x-goog-spanner-request-id to the currently active span.
attributeXGoogSpannerRequestIdToActiveSpan(config);
}
const interceptors: any[] = [];
if (this._metricsEnabled) {
interceptors.push(MetricInterceptor);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const customInterceptors: any[] =
config.gaxOpts?.otherArgs?.options?.interceptors ?? [];
const interceptors = this._metricsEnabled
? [...customInterceptors, MetricInterceptor]
: customInterceptors;
const headers = Object.assign(
{},
config.gaxOpts?.otherArgs?.headers,
config.headers,
);
const options = Object.assign({}, config.gaxOpts?.otherArgs?.options, {
interceptors,
});
const gaxOpts = extend(true, {}, config.gaxOpts, {
otherArgs: {
headers,
options,
},
});
const requestFn = gaxClient[config.method].bind(
gaxClient,
reqOpts,
// Add headers to `gaxOpts`
extend(true, {}, config.gaxOpts, {
otherArgs: {
headers: config.headers,
options: {
interceptors: interceptors,
},
},
}),
gaxOpts,
);

// Wrap requestFn to inject the spanner request id into every returned error.
const wrappedRequestFn = (...args) => {
wrappedRequestFn = (...args) => {
const hasCallback =
args &&
args.length > 0 &&
Expand All @@ -1814,33 +1912,38 @@ class Spanner extends GrpcService {
}

case false: {
const res = requestFn(...args);
const stream = res as EventEmitter;
if (stream) {
stream.on('error', err => {
let res;
try {
res = requestFn(...args);
} catch (err) {
injectRequestIDIntoError(config, err as Error);
throw err;
}

if (res instanceof Promise) {
return res.catch(err => {
injectRequestIDIntoError(config, err as Error);
throw err;
});
}

const originallyPromise = res instanceof Promise;
if (!originallyPromise) {
return res;
const stream = res as EventEmitter;
if (stream && typeof stream.on === 'function') {
stream.on('error', err => {
injectRequestIDIntoError(config, err as Error);
});
}

return new Promise((resolve, reject) => {
requestFn(...args)
.then(resolve)
.catch(err => {
injectRequestIDIntoError(config, err as Error);
reject(err);
});
});
return res;
}
}
};
} catch (error) {
callback(error as Error, null);
return;
}

callback(null, wrappedRequestFn);
});
callback(null, wrappedRequestFn);
}

/**
Expand All @@ -1866,7 +1969,7 @@ class Spanner extends GrpcService {
MetricsTracerFactory?.getInstance(this.projectId_)?.createMetricsTracer(
config.method,
config.reqOpts.database ?? config.reqOpts.session,
config.headers['x-goog-spanner-request-id'],
config.headers?.['x-goog-spanner-request-id'],
) ?? null;
}
metricsTracer?.recordOperationStart();
Expand Down Expand Up @@ -1896,6 +1999,7 @@ class Spanner extends GrpcService {
.then(val => {
metricsTracer?.recordOperationCompletion();
resolve(val);
return val;
})
.catch(error => {
metricsTracer?.recordOperationCompletion();
Expand Down Expand Up @@ -1934,7 +2038,7 @@ class Spanner extends GrpcService {
MetricsTracerFactory?.getInstance(this.projectId_)?.createMetricsTracer(
config.method,
config.reqOpts.session ?? config.reqOpts.database,
config.headers['x-goog-spanner-request-id'],
config.headers?.['x-goog-spanner-request-id'],
) ?? null;
}
metricsTracer?.recordOperationStart();
Expand Down
Loading
Loading