diff --git a/src/dataset.ts b/src/dataset.ts index 227ff68a7..38d2aba80 100644 --- a/src/dataset.ts +++ b/src/dataset.ts @@ -44,6 +44,7 @@ import { TableOptions, } from './table'; import {Model} from './model'; +import {Routine} from './routine'; import bigquery from './types'; export interface DatasetDeleteOptions { @@ -68,6 +69,18 @@ export type GetModelsCallback = PagedCallback< bigquery.IListModelsResponse >; +export type GetRoutinesOptions = PagedRequest; +export type GetRoutinesResponse = PagedResponse< + Routine, + GetRoutinesOptions, + bigquery.IListRoutinesResponse +>; +export type GetRoutinesCallback = PagedCallback< + Routine, + GetRoutinesOptions, + bigquery.IListRoutinesResponse +>; + export type GetTablesOptions = PagedRequest; export type GetTablesResponse = PagedResponse< Table, @@ -80,6 +93,10 @@ export type GetTablesCallback = PagedCallback< bigquery.ITableList >; +export type RoutineMetadata = bigquery.IRoutine; +export type RoutineResponse = [Routine, bigquery.IRoutine]; +export type RoutineCallback = ResourceCallback; + export type TableResponse = [Table, bigquery.ITable]; export type TableCallback = ResourceCallback; @@ -103,6 +120,7 @@ class Dataset extends ServiceObject { bigQuery: BigQuery; location?: string; getModelsStream: (options?: GetModelsOptions) => ResourceStream; + getRoutinesStream: (options?: GetRoutinesOptions) => ResourceStream; getTablesStream: (options?: GetTablesOptions) => ResourceStream; constructor(bigQuery: BigQuery, id: string, options?: DatasetOptions) { const methods = { @@ -330,6 +348,34 @@ class Dataset extends ServiceObject { */ this.getModelsStream = paginator.streamify('getModels'); + /** + * List all or some of the {@link Routine} objects in your project as a + * readable object stream. + * + * @method Dataset#getRoutinesStream + * @param {GetRoutinesOptions} [options] Configuration object. + * @returns {stream} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('institutions'); + * + * dataset.getRoutinesStream() + * .on('error', console.error) + * .on('data', (routine) => {}) + * .on('end', () => { + * // All routines have been retrieved + * }); + * + * @example + * dataset.getRoutinesStream() + * .on('data', function(routine) { + * this.end(); + * }); + */ + this.getRoutinesStream = paginator.streamify('getRoutines'); + /** * List all or some of the {module:bigquery/table} objects in your project * as a readable object stream. @@ -422,6 +468,94 @@ class Dataset extends ServiceObject { return this.bigQuery.createQueryStream(options); } + createRoutine(id: string, config: RoutineMetadata): Promise; + createRoutine( + id: string, + config: RoutineMetadata, + callback: RoutineCallback + ): void; + /** + * @callback CreateRoutineCallback + * @param {?Error} err Request error, if any. + * @param {Routine} routine The newly created routine. + * @param {object} response The full API response body. + */ + /** + * @typedef {array} CreateRoutineResponse + * @property {Routine} 0 The newly created routine. + * @property {object} 1 The full API response body. + */ + /** + * Create a routine. + * + * @see [Routines: insert API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/insert} + * + * @param {string} id The routine ID. + * @param {object} config A [routine resource]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines#Routine}. + * @param {CreateRoutineCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * + * const id = 'my-routine'; + * const config = { + * arguments: [{ + * name: 'x', + * dataType: { + * typeKind: 'INT64' + * } + * }], + * definitionBody: 'x * 3', + * routineType: 'SCALAR_FUNCTION', + * returnType: { + * typeKind: 'INT64' + * } + * }; + * + * dataset.createRoutine(id, config, (err, routine, apiResponse) => { + * if (!err) { + * // The routine was created successfully. + * } + * }); + * + * @example + * const [routine, apiResponse] = await dataset.createRoutine(id, config); + */ + createRoutine( + id: string, + config: RoutineMetadata, + callback?: RoutineCallback + ): void | Promise { + const json = Object.assign({}, config, { + routineReference: { + routineId: id, + datasetId: this.id, + projectId: this.bigQuery.projectId, + }, + }); + + this.request( + { + method: 'POST', + uri: '/routines', + json, + }, + (err, resp) => { + if (err) { + callback!(err, null, resp); + return; + } + + const routine = this.routine(resp.routineReference.routineId); + routine.metadata = resp; + callback!(null, routine, resp); + } + ); + } + createTable(id: string, options: TableMetadata): Promise; createTable( id: string, @@ -654,6 +788,102 @@ class Dataset extends ServiceObject { ); } + getRoutines(options?: GetRoutinesOptions): Promise; + getRoutines(options: GetRoutinesOptions, callback: GetRoutinesCallback): void; + getRoutines(callback: GetRoutinesCallback): void; + /** + * @typedef {object} GetRoutinesOptions + * @property {boolean} [autoPaginate=true] Have pagination handled + * automatically. + * @property {number} [maxApiCalls] Maximum number of API calls to make. + * @property {number} [maxResults] Maximum number of results to return. + * @property {string} [pageToken] Token returned from a previous call, to + * request the next page of results. + */ + /** + * @callback GetRoutinesCallback + * @param {?Error} err Request error, if any. + * @param {Routine[]} routines List of routine objects. + * @param {GetRoutinesOptions} nextQuery If `autoPaginate` is set to true, + * this will be a prepared query for the next page of results. + * @param {object} response The full API response. + */ + /** + * @typedef {array} GetRoutinesResponse + * @property {Routine[]} 0 List of routine objects. + * @property {GetRoutinesOptions} 1 If `autoPaginate` is set to true, this + * will be a prepared query for the next page of results. + * @property {object} 2 The full API response. + */ + /** + * Get a list of routines. + * + * @see [Routines: list API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/list} + * + * @param {GetRoutinesOptions} [options] Request options. + * @param {GetRoutinesCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('institutions'); + * + * dataset.getRoutines((err, routines) => { + * // routines is an array of `Routine` objects. + * }); + * + * @example + * function manualPaginationCallback(err, routines, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * dataset.getRoutines(nextQuery, manualPaginationCallback); + * } + * } + * + * dataset.getRoutines({ + * autoPaginate: false + * }, manualPaginationCallback); + * + * @example + * const [routines] = await dataset.getRoutines(); + */ + getRoutines( + optsOrCb?: GetRoutinesOptions | GetRoutinesCallback, + cb?: GetRoutinesCallback + ): void | Promise { + const options = typeof optsOrCb === 'object' ? optsOrCb : {}; + const callback = typeof optsOrCb === 'function' ? optsOrCb : cb; + + this.request( + { + uri: '/routines', + qs: options, + }, + (err: Error | null, resp: bigquery.IListRoutinesResponse) => { + if (err) { + callback!(err, null, null, resp); + return; + } + + let nextQuery: {} | null = null; + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken, + }); + } + + const routines = (resp.routines || []).map(metadata => { + const routine = this.routine(metadata.routineReference!.routineId!); + routine.metadata = metadata; + return routine; + }); + + callback!(null, routines, nextQuery, resp); + } + ); + } + getTables(options?: GetTablesOptions): Promise; getTables(options: GetTablesOptions, callback: GetTablesCallback): void; getTables(callback: GetTablesCallback): void; @@ -799,6 +1029,29 @@ class Dataset extends ServiceObject { return this.bigQuery.query(options, callback); } + /** + * Create a Routine object. + * + * @throws {TypeError} if routine ID is missing. + * + * @param {string} id The ID of the routine. + * @returns {Routine} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('institutions'); + * + * const routine = dataset.routine('my_routine'); + */ + routine(id: string): Routine { + if (typeof id !== 'string') { + throw new TypeError('A routine ID is required.'); + } + + return new Routine(this, id); + } + /** * Create a Table object. * @@ -840,7 +1093,7 @@ class Dataset extends ServiceObject { * * These methods can be auto-paginated. */ -paginator.extend(Dataset, ['getModels', 'getTables']); +paginator.extend(Dataset, ['getModels', 'getRoutines', 'getTables']); /*! Developer Documentation * @@ -848,7 +1101,7 @@ paginator.extend(Dataset, ['getModels', 'getTables']); * that a callback is omitted. */ promisifyAll(Dataset, { - exclude: ['model', 'table'], + exclude: ['model', 'routine', 'table'], }); /** diff --git a/src/index.ts b/src/index.ts index ce74f7923..76305a4f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,9 +57,15 @@ export { GetModelsCallback, GetModelsOptions, GetModelsResponse, + GetRoutinesCallback, + GetRoutinesOptions, + GetRoutinesResponse, GetTablesCallback, GetTablesOptions, GetTablesResponse, + RoutineCallback, + RoutineMetadata, + RoutineResponse, TableCallback, TableResponse, } from './dataset'; @@ -103,4 +109,5 @@ export { ViewDefinition, } from './table'; +export {Routine} from './routine'; export {Model} from './model'; diff --git a/src/routine.ts b/src/routine.ts new file mode 100644 index 000000000..761e2f920 --- /dev/null +++ b/src/routine.ts @@ -0,0 +1,291 @@ +/*! + * Copyright 2020 Google LLC + * + * 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. + */ + +import * as common from '@google-cloud/common'; +import {promisifyAll} from '@google-cloud/promisify'; +import extend = require('extend'); + +import {Dataset, RoutineMetadata} from './dataset'; + +/** + * Routine objects are returned by methods such as + * {@link Dataset#routine}, {@link Dataset#createRoutine}, and + * {@link Dataset#getRoutines}. + * + * @class + * @param {Dataset} dataset {@link Dataset} instance. + * @param {string} id The ID of the routine. + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * + * const routine = dataset.routine('my_routine'); + */ +class Routine extends common.ServiceObject { + constructor(dataset: Dataset, id: string) { + const methods = { + /** + * Create a routine. + * + * @see [Routines: insert API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/insert} + * + * @method Routine#create + * @param {object} config A [routine resource]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines#Routine}. + * @param {CreateRoutineCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * const routine = dataset.routine('my_routine'); + * + * const config = { + * arguments: [{ + * name: 'x', + * dataType: { + * typeKind: 'INT64' + * } + * }], + * definitionBody: 'x * 3', + * routineType: 'SCALAR_FUNCTION', + * returnType: { + * typeKind: 'INT64' + * } + * }; + * + * routine.create(config, (err, routine, apiResponse) => { + * if (!err) { + * // The routine was created successfully. + * } + * }); + * + * @example + * const [routine, apiResponse] = await routine.create(config); + */ + create: true, + + /** + * @callback DeleteRoutineCallback + * @param {?Error} err Request error, if any. + * @param {object} apiResponse The full API response. + */ + /** + * @typedef {array} DeleteRoutineResponse + * @property {object} 0 The full API response. + */ + /** + * Deletes a routine. + * + * @see [Routines: delete API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/delete} + * + * @method Routine#delete + * @param {DeleteRoutineCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * const routine = dataset.routine('my_routine'); + * + * routine.delete((err, apiResponse) => {}); + * + * @example + * const [apiResponse] = await routine.delete(); + */ + delete: true, + + /** + * @callback RoutineExistsCallback + * @param {?Error} err Request error, if any. + * @param {boolean} exists Indicates if the routine exists. + */ + /** + * @typedef {array} RoutineExistsResponse + * @property {boolean} 0 Indicates if the routine exists. + */ + /** + * Check if the routine exists. + * + * @method Routine#exists + * @param {RoutineExistsCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * const routine = dataset.routine('my_routine'); + * + * routine.exists((err, exists) => {}); + * + * @example + * const [exists] = await routine.exists(); + */ + exists: true, + + /** + * @callback GetRoutineCallback + * @param {?Error} err Request error, if any. + * @param {Routine} routine The routine. + * @param {object} apiResponse The full API response body. + */ + /** + * @typedef {array} GetRoutineResponse + * @property {Routine} 0 The routine. + * @property {object} 1 The full API response body. + */ + /** + * Get a routine if it exists. + * + * @see [Routines: get API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/get} + * + * @method Routine#get + * @param {GetRoutineCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * const routine = dataset.routine('my_routine'); + * + * routine.get((err, routine) => {}); + * + * @example + * const [routine2] = await routine.get(); + */ + get: true, + + /** + * @callback GetRoutineMetadataCallback + * @param {?Error} err Request error, if any. + * @param {object} metadata The routine metadata. + * @param {object} apiResponse The full API response. + */ + /** + * @typedef {array} GetRoutineMetadataResponse + * @property {object} 0 The routine metadata. + * @property {object} 1 The full API response. + */ + /** + * Get the metadata associated with a routine. + * + * @see [Routines: get API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/get} + * + * @method Routine#getMetadata + * @param {GetRoutineMetadataCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * const routine = dataset.routine('my_routine'); + * + * routine.getMetadata((err, metadata, apiResponse) => {}); + * + * @example + * const [metadata, apiResponse] = await routine.getMetadata(); + */ + getMetadata: true, + + /** + * @callback SetRoutineMetadataCallback + * @param {?Error} err Request error, if any. + * @param {object} metadata The routine metadata. + * @param {object} apiResponse The full API response. + */ + /** + * @typedef {array} SetRoutineMetadataResponse + * @property {object} 0 The routine metadata. + * @property {object} 1 The full API response. + */ + /** + * Update a routine. + * + * @see [Routines: update API Documentation]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/update} + * + * @method Routine#setMetadata + * @param {object} metadata A [routine resource object]{@link https://cloud.google.com/bigquery/docs/reference/rest/v2/routines#Routine}. + * @param {SetRoutineMetadataCallback} [callback] The callback function. + * @returns {Promise} + * + * @example + * const {BigQuery} = require('@google-cloud/bigquery'); + * const bigquery = new BigQuery(); + * const dataset = bigquery.dataset('my-dataset'); + * const routine = dataset.routine('my_routine'); + * + * const updates = { + * description: 'The perfect description!' + * }; + * + * routine.setMetadata(updates, (err, metadata, apiResponse) => {}); + * + * @example + * const [metadata, apiResponse] = await routine.setMetadata(updates); + */ + setMetadata: { + reqOpts: { + method: 'PUT', + }, + }, + }; + + super({ + parent: dataset, + baseUrl: '/routines', + id, + methods, + createMethod: dataset.createRoutine.bind(dataset, id), + }); + } + + setMetadata(metadata: RoutineMetadata): Promise; + setMetadata( + metadata: RoutineMetadata, + callback: common.ResponseCallback + ): void; + setMetadata( + metadata: RoutineMetadata, + callback?: common.ResponseCallback + ): void | Promise { + // per the python client, it would appear that in order to update a routine + // you need to send the routine in its entirety, not just the updated fields + this.getMetadata((err: Error | null, fullMetadata: RoutineMetadata) => { + if (err) { + callback!(err); + return; + } + + const updatedMetadata = extend(true, {}, fullMetadata, metadata); + super.setMetadata(updatedMetadata, callback!); + }); + } +} + +/*! Developer Documentation + * + * All async methods (except for streams) will return a Promise in the event + * that a callback is omitted. + */ +promisifyAll(Routine); + +export {Routine}; diff --git a/system-test/bigquery.ts b/system-test/bigquery.ts index 7e8cb2011..b49f155bc 100644 --- a/system-test/bigquery.ts +++ b/system-test/bigquery.ts @@ -31,6 +31,7 @@ import { Job, Model, RowMetadata, + Routine, Table, } from '../src'; @@ -675,6 +676,79 @@ describe('BigQuery', () => { }); }); + describe('BigQuery/Routine', () => { + before(() => { + const routineId = `${bigquery.projectId}.${dataset.id}.my_ddl_routine`; + + return bigquery.query(` + CREATE FUNCTION \`${routineId}\`( + arr ARRAY> + ) AS ( + (SELECT SUM(IF(elem.name = "foo",elem.val,null)) FROM UNNEST(arr) AS elem) + ) + `); + }); + + after(async () => { + const [routines] = await dataset.getRoutines(); + return Promise.all(routines.map(routine => routine.delete())); + }); + + it('should create a routine via insert', () => { + return dataset.createRoutine('my_routine', { + arguments: [ + { + name: 'x', + dataType: { + typeKind: 'INT64', + }, + }, + ], + definitionBody: 'x * 3', + routineType: 'SCALAR_FUNCTION', + returnType: { + typeKind: 'INT64', + }, + }); + }); + + it('should list all the routines', async () => { + const [routines] = await dataset.getRoutines(); + assert.ok(routines.length > 0); + assert.ok(routines[0] instanceof Routine); + }); + + it('should get the routines as a stream', done => { + const routines: Routine[] = []; + + dataset + .getRoutinesStream() + .on('error', done) + .on('data', routine => { + routines.push(routine); + }) + .on('end', () => { + assert.ok(routines.length > 0); + assert.ok(routines[0] instanceof Routine); + done(); + }); + }); + + it('should check to see if a routine exists', async () => { + const routine = dataset.routine('my_ddl_routine'); + const [exists] = await routine.exists(); + assert.ok(exists); + }); + + it('should update an existing routine', async () => { + const routine = dataset.routine('my_ddl_routine'); + const description = 'A routine!'; + + await routine.setMetadata({description}); + assert.strictEqual(routine.metadata.description, description); + }); + }); + describe('BigQuery/Table', () => { const TEST_DATA_JSON_PATH = require.resolve( '../../system-test/data/kitten-test-data.json' diff --git a/test/dataset.ts b/test/dataset.ts index 8b7cdfbe2..551e90917 100644 --- a/test/dataset.ts +++ b/test/dataset.ts @@ -36,7 +36,7 @@ const fakePfy = extend({}, pfy, { return; } promisified = true; - assert.deepStrictEqual(options.exclude, ['model', 'table']); + assert.deepStrictEqual(options.exclude, ['model', 'routine', 'table']); }, }); @@ -49,7 +49,11 @@ const fakePaginator = { } methods = arrify(methods); assert.strictEqual(c.name, 'Dataset'); - assert.deepStrictEqual(methods, ['getModels', 'getTables']); + assert.deepStrictEqual(methods, [ + 'getModels', + 'getRoutines', + 'getTables', + ]); extended = true; }, streamify: (methodName: string) => {
If you anticipate many results, you can end a stream early to prevent unnecessary processing and API requests.If the callback is omitted a Promise will be returnedTo control how many API requests are made and page through the results manually, set `autoPaginate` to `false`.If the callback is omitted a Promise will be returnedIf the callback is omitted a Promise will be returnedIf the callback is omitted a Promise will be returnedIf the callback is omitted a Promise will be returnedIf the callback is omitted a Promise will be returnedIf the callback is omitted a Promise will be returnedIf the callback is omitted a Promise will be returned