From ef1c96d9bc1ae7c27b6ec4b07c7d33f04930d2a7 Mon Sep 17 00:00:00 2001 From: praphun <108100102+praphun@users.noreply.github.com> Date: Fri, 5 Aug 2022 12:13:23 -0700 Subject: [PATCH] fix: add field details for SDK error (#1134) * fix: parse response from 204 No Content * fix: add field details for SDK error --- packages/sdk-node/src/nodeTransport.ts | 2 +- packages/sdk-node/test/methods.spec.ts | 41 ++++++ packages/sdk-rtl/src/index.ts | 1 + packages/sdk-rtl/src/lookerSDKError.ts | 185 +++++++++++++++++++++++++ packages/sdk-rtl/src/transport.ts | 20 +-- 5 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 packages/sdk-rtl/src/lookerSDKError.ts diff --git a/packages/sdk-node/src/nodeTransport.ts b/packages/sdk-node/src/nodeTransport.ts index 1d36fcb43..74e225f1e 100644 --- a/packages/sdk-node/src/nodeTransport.ts +++ b/packages/sdk-node/src/nodeTransport.ts @@ -194,7 +194,7 @@ export class NodeTransport extends BaseTransport { } } else { try { - result = Buffer.from(result).toString('binary') + result = Buffer.from(result || '').toString('binary') } catch (err) { error = err } diff --git a/packages/sdk-node/test/methods.spec.ts b/packages/sdk-node/test/methods.spec.ts index c56f1b98a..da7c31e29 100644 --- a/packages/sdk-node/test/methods.spec.ts +++ b/packages/sdk-node/test/methods.spec.ts @@ -52,6 +52,7 @@ import { ApiConfigMap, pageAll, pager, + LookerSDKError, } from '@looker/sdk-rtl' import { NodeSettings, @@ -1088,6 +1089,46 @@ describe('LookerNodeSDK', () => { ) }) + describe('Theme', () => { + it('validate_theme returns ok on valid template', async () => { + const sdk = new LookerSDK(session) + const result = await sdk.ok( + sdk.validate_theme({ + name: 'validTemplate', + settings: { + show_filters_bar: false, + show_title: false, + tile_shadow: false, + font_family: 'Arial', + }, + }) + ) + expect(result).toBeDefined() + expect(result).toEqual('') + }) + + it('validate_theme throws error with details', async () => { + const sdk = new LookerSDK(session) + try { + await sdk.ok( + sdk.validate_theme({ + settings: { + show_filters_bar: false, + show_title: false, + tile_shadow: false, + font_family: 'Arial;', + }, + }) + ) + } catch (e: any) { + expect(e).toBeInstanceOf(LookerSDKError) + expect(e.message).toBeDefined() + expect(e.errors).toBeDefined() + expect(e.errors).toHaveLength(3) + } + }) + }) + describe('Node environment', () => { beforeAll(() => { const section = readIniConfig( diff --git a/packages/sdk-rtl/src/index.ts b/packages/sdk-rtl/src/index.ts index 697d3413e..5a222ec81 100644 --- a/packages/sdk-rtl/src/index.ts +++ b/packages/sdk-rtl/src/index.ts @@ -38,6 +38,7 @@ export * from './CSRFSession' export * from './delimArray' export * from './extensionSession' export * from './extensionTransport' +export * from './lookerSDKError' export * from './oauthSession' export * from './paging' export * from './platformServices' diff --git a/packages/sdk-rtl/src/lookerSDKError.ts b/packages/sdk-rtl/src/lookerSDKError.ts new file mode 100644 index 000000000..3345b900d --- /dev/null +++ b/packages/sdk-rtl/src/lookerSDKError.ts @@ -0,0 +1,185 @@ +/* + + MIT License + + Copyright (c) 2022 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ + +/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-redeclare */ + +// Defensive type-level programming to guard against additional +// parameters being added to Error, as happened in lib.es2022.error.d.ts +// (which introduced the errorOptions argument) +type AugmentErrorOptions< + ErrorParameters extends unknown[], + AdditionalErrorOptions +> = ErrorParameters extends [(infer Message)?] + ? [Message?, AdditionalErrorOptions?] + : ErrorParameters extends [(infer Message)?, (infer ErrorOptions)?] + ? [Message?, (ErrorOptions & AdditionalErrorOptions)?] + : ErrorParameters extends [ + (infer Message)?, + (infer ErrorOptions)?, + ...infer Rest + ] + ? [Message?, (ErrorOptions & AdditionalErrorOptions)?, ...Rest] + : never + +interface IErrorDetail { + field?: string | null + code?: string | null + message?: string | null + documentation_url: string | null +} + +// This specifies SDK custom error options +interface ILookerSDKErrorOptions { + errors?: IErrorDetail[] + documentation_url?: string | null +} + +interface ILookerSDKErrorConstructor { + new ( + ...args: AugmentErrorOptions< + ConstructorParameters, + ILookerSDKErrorOptions + > + ): LookerSDKError + ( + ...args: AugmentErrorOptions< + Parameters, + ILookerSDKErrorOptions + > + ): LookerSDKError +} + +// The subclass and function expression's name should match, so that stack traces look clean. +// We bind it to a local identifier for clarity, and to perform a type assertion. +export interface LookerSDKError extends Error { + errors?: IErrorDetail[] + documentation_url?: string | null +} + +export const LookerSDKError: ILookerSDKErrorConstructor = + /* #__PURE__*/ (() => { + 'use strict' + const LookerSDKErrorConstructor = function LookerSDKError( + this: LookerSDKError | undefined, + ...[ + message, + { errors = [], documentation_url = '', ...errorOptions } = {}, + ...rest + ]: AugmentErrorOptions< + ConstructorParameters, + ILookerSDKErrorOptions + > & + AugmentErrorOptions< + Parameters, + ILookerSDKErrorOptions + > + ) { + // The `super()` call. At present, Error() and new Error() are + // indistinguishable, but use whatever we were invoked with in case + // that ever changes. + const error = this + ? new Error( + message, + // we have to suppress a type error here if TypeScript + // doesn't know es2022's two-argument Error constructor + // @ts-ignore-error + errorOptions, + ...rest + ) + : Error( + message, + // @ts-ignore-error + errorOptions, + ...rest + ) + + // Object.setPrototypeOf() is necessary when extending built-ins, + // since Error.call(this, message, errorOptions, ...rest) doesn't + // set up the prototype chain the way it would with a user-defined + // class. + Object.setPrototypeOf( + error, + this ? Object.getPrototypeOf(this) : LookerSDKError.prototype + ) + + // imitates the non-enumerability of the 'message' and 'stack' + // instance properties on built-in Error. This may not be desirable + // for your custom error. + Object.defineProperties(error, { + errors: { + value: errors, + writable: true, + configurable: true, + }, + documentation_url: { + value: documentation_url, + writable: true, + configurable: true, + }, + }) + + return error + } as ILookerSDKErrorConstructor + + // LookerSDKError.prototype, LookerSDKError.prototype.constructor, and + // LookerSDKError.prototoype.name all have to be non-enumerable to match + // the built-in RangeError, TypeError, etc., so we use + // Object.defineProperty to set them instead of `=` assignment. + + // Default values for property descriptor objects: + // writable: false + // enumerable: false + // configurable: false + Object.defineProperty( + LookerSDKErrorConstructor, + 'prototype', + // It's a weird wart that the built-in Error constructor + // prototypes have writable and configurable constructor and name + // fields. We follow that behavior to be consistent, not because + // it makes sense. + { + value: Object.create(Error.prototype, { + constructor: { + value: LookerSDKErrorConstructor, + writable: true, + configurable: true, + }, + name: { + value: 'LookerSDKError', + writable: true, + configurable: true, + }, + }), + // SomeConstructorFunction.prototype starts off writable with + // `function`-type constructors, in contrast to `class SomeClass`. + // Set this to be non-writable to match `class`es and the built-in + // Error constructors. + writable: false, + } + ) + + return LookerSDKErrorConstructor + })() diff --git a/packages/sdk-rtl/src/transport.ts b/packages/sdk-rtl/src/transport.ts index 01728cce5..1424ad851 100644 --- a/packages/sdk-rtl/src/transport.ts +++ b/packages/sdk-rtl/src/transport.ts @@ -29,6 +29,7 @@ import type { Headers } from 'request' import type { Readable } from 'readable-stream' import { matchCharsetUtf8, matchModeBinary, matchModeString } from './constants' import { DelimArray } from './delimArray' +import { LookerSDKError } from './lookerSDKError' export const agentPrefix = 'TS-SDK' export const LookerAppId = 'x-looker-appid' @@ -470,32 +471,35 @@ function bufferString(val: any) { */ export function sdkError(response: any) { if (typeof response === 'string') { - return new Error(response) + return new LookerSDKError(response) } if ('error' in response) { const error = response.error if (typeof error === 'string') { - return new Error(error) + return new LookerSDKError(error) } // Try to get most specific error first if ('error' in error) { const result = bufferString(error.error) - return new Error(result) + return new LookerSDKError(result) } if ('message' in error) { - return new Error(response.error.message.toString()) + return new LookerSDKError(response.error.message.toString(), { + errors: error.errors, + documentation_url: error.documentation_url, + }) } if ('statusMessage' in error) { - return new Error(error.statusMessage) + return new LookerSDKError(error.statusMessage) } const result = bufferString(error) - return new Error(result) + return new LookerSDKError(result) } if ('message' in response) { - return new Error(response.message) + return new LookerSDKError(response.message) } const error = JSON.stringify(response) - return new Error(`Unknown error with SDK method ${error}`) + return new LookerSDKError(`Unknown error with SDK method ${error}`) } /** A helper method for simplifying error handling of SDK responses.