Bläddra i källkod

HUE-9376 [ui] Move the SQL reference repository to Typescript

This also takes care of a couple of minor bugs found during the conversion
Johan Ahlen 5 år sedan
förälder
incheckning
3be8d30d38

+ 28 - 18
desktop/core/src/desktop/js/sql/reference/apiUtils.test.js → desktop/core/src/desktop/js/sql/reference/apiUtils.test.ts

@@ -14,45 +14,45 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { adaptApiFunctions, extractArgumentTypes, mergeArgumentTypes } from './apiUtils';
+import { adaptApiFunctions, ApiUdf, extractArgumentTypes, mergeArgumentTypes } from './apiUtils';
 
 describe('apiUtils.js', () => {
   it('should return the default signature when not defined', () => {
-    const result = extractArgumentTypes({});
+    const result = extractArgumentTypes({ name: 'foo' });
     expect(JSON.stringify(result)).toEqual(JSON.stringify([[{ type: 'T', multiple: true }]]));
   });
 
   it('should extract empty argument types from empty signature', () => {
-    const result = extractArgumentTypes({ signature: '()' });
+    const result = extractArgumentTypes({ name: 'foo', signature: '()' });
     expect(result.length).toEqual(0);
   });
 
   it('should extract single argument type from signature', () => {
-    const result = extractArgumentTypes({ signature: '(INT)' });
+    const result = extractArgumentTypes({ name: 'foo', signature: '(INT)' });
     expect(JSON.stringify(result)).toEqual(JSON.stringify([[{ type: 'INT' }]]));
   });
 
   it('should extract multiple argument types from signature', () => {
-    const result = extractArgumentTypes({ signature: '(INT, BIGINT, TINYINT)' });
+    const result = extractArgumentTypes({ name: 'foo', signature: '(INT, BIGINT, TINYINT)' });
     expect(JSON.stringify(result)).toEqual(
       JSON.stringify([[{ type: 'INT' }], [{ type: 'BIGINT' }], [{ type: 'TINYINT' }]])
     );
   });
 
   it('should ignore precision in the signature', () => {
-    const result = extractArgumentTypes({ signature: '(DECIMAL(*,*))' });
+    const result = extractArgumentTypes({ name: 'foo', signature: '(DECIMAL(*,*))' });
     expect(JSON.stringify(result)).toEqual(JSON.stringify([[{ type: 'DECIMAL' }]]));
   });
 
   it('should support repetitive argument types from signature', () => {
-    const result = extractArgumentTypes({ signature: '(INT, BIGINT...)' });
+    const result = extractArgumentTypes({ name: 'foo', signature: '(INT, BIGINT...)' });
     expect(JSON.stringify(result)).toEqual(
       JSON.stringify([[{ type: 'INT' }], [{ type: 'BIGINT', multiple: true }]])
     );
   });
 
   it('should support repetitive argument types with precision from signature', () => {
-    const result = extractArgumentTypes({ signature: '(INT, CHAR(*)...)' });
+    const result = extractArgumentTypes({ name: 'foo', signature: '(INT, CHAR(*)...)' });
     expect(JSON.stringify(result)).toEqual(
       JSON.stringify([[{ type: 'INT' }], [{ type: 'CHAR', multiple: true }]])
     );
@@ -67,6 +67,24 @@ describe('apiUtils.js', () => {
     expect(JSON.stringify(target)).toEqual(JSON.stringify([[{ type: 'INT' }, { type: 'DOUBLE' }]]));
   });
 
+  it('should merge types with target as T', () => {
+    const target = [[{ type: 'T' }]];
+    const additional = [[{ type: 'DOUBLE' }]];
+
+    mergeArgumentTypes(target, additional);
+
+    expect(JSON.stringify(target)).toEqual(JSON.stringify([[{ type: 'T' }]]));
+  });
+
+  it('should merge types with additional as T', () => {
+    const target = [[{ type: 'STRING' }]];
+    const additional = [[{ type: 'T' }]];
+
+    mergeArgumentTypes(target, additional);
+
+    expect(JSON.stringify(target)).toEqual(JSON.stringify([[{ type: 'T' }]]));
+  });
+
   it('should add arguments where missing', () => {
     const apiFunctions = [
       {
@@ -108,17 +126,13 @@ describe('apiUtils.js', () => {
   });
 
   it('should merge same udf with multiple argument types', () => {
-    const apiFunctions = [
+    const apiFunctions: ApiUdf[] = [
       {
-        is_builtin: 'BUILTIN',
-        is_persistent: 'true',
         name: 'casttochar',
         return_type: 'CHAR(*)',
         signature: '(CHAR(*))'
       },
       {
-        is_builtin: 'BUILTIN',
-        is_persistent: 'true',
         name: 'casttochar',
         return_type: 'CHAR(*)',
         signature: '(INT)'
@@ -135,17 +149,13 @@ describe('apiUtils.js', () => {
   });
 
   it('should merge same udf with multiple return types', () => {
-    const apiFunctions = [
+    const apiFunctions: ApiUdf[] = [
       {
-        is_builtin: 'BUILTIN',
-        is_persistent: 'true',
         name: 'casttochar',
         return_type: 'CHAR(*)',
         signature: '(CHAR(*))'
       },
       {
-        is_builtin: 'BUILTIN',
-        is_persistent: 'true',
         name: 'casttochar',
         return_type: 'VARCHAR(*)',
         signature: '(INT)'

+ 49 - 35
desktop/core/src/desktop/js/sql/reference/apiUtils.js → desktop/core/src/desktop/js/sql/reference/apiUtils.ts

@@ -16,31 +16,43 @@
 
 import { simplePostAsync } from 'api/apiUtils';
 import { AUTOCOMPLETE_API_PREFIX } from 'api/urls';
+import { Argument, Connector, UdfDetails } from './sqlReferenceRepository';
 import I18n from 'utils/i18n';
 
+export interface ApiUdf {
+  name: string;
+  is_builtin?: string;
+  is_persistent?: string;
+  return_type?: string;
+  signature?: string;
+}
+
 const FUNCTIONS_OPERATION = 'functions';
 const DEFAULT_DESCRIPTION = I18n('No description available.');
-const DEFAULT_RETURN_TYPE = ['T'];
+const DEFAULT_RETURN_TYPES = ['T'];
 const DEFAULT_ARGUMENTS = [[{ type: 'T', multiple: true }]];
 
 const SIGNATURE_REGEX = /([a-z]+(?:\.{3})?)/gi;
 const TYPE_REGEX = /(?<type>[a-z]+)(?<multiple>\.{3})?/i;
 
-const stripPrecision = typeString => typeString.replace(/\(\*(,\*)?\)/g, '');
+const stripPrecision = (typeString: string): string => typeString.replace(/\(\*(,\*)?\)/g, '');
 
-// TODO: Extend with arguments etc reported by the API
-export const adaptApiUdf = apiUdf => {
+const adaptApiUdf = (apiUdf: ApiUdf): UdfDetails => {
   const signature = apiUdf.name + '()';
   return {
-    returnTypes: apiUdf.returnTypes || DEFAULT_RETURN_TYPE,
-    arguments: apiUdf.arguments || DEFAULT_ARGUMENTS,
+    name: apiUdf.name,
+    returnTypes: extractReturnTypes(apiUdf),
+    arguments: extractArgumentTypes(apiUdf),
     signature: signature,
     draggable: signature,
     description: DEFAULT_DESCRIPTION
   };
 };
 
-export const extractArgumentTypes = apiUdf => {
+const extractReturnTypes = (apiUdf: ApiUdf): string[] =>
+  apiUdf.return_type ? [stripPrecision(apiUdf.return_type)] : DEFAULT_RETURN_TYPES;
+
+export const extractArgumentTypes = (apiUdf: ApiUdf): Argument[][] => {
   if (apiUdf.signature) {
     const cleanSignature = stripPrecision(apiUdf.signature);
     if (cleanSignature === '()') {
@@ -50,26 +62,30 @@ export const extractArgumentTypes = apiUdf => {
     if (match) {
       return match.map(argString => {
         const typeMatch = argString.match(TYPE_REGEX);
-        const arg = { type: typeMatch.groups.type };
-        if (typeMatch.groups.multiple) {
-          arg.multiple = true;
+        if (typeMatch && typeMatch.groups) {
+          const arg: Argument = { type: typeMatch.groups.type };
+          if (typeMatch.groups.multiple) {
+            arg.multiple = true;
+          }
+          return [arg];
+        } else {
+          return [];
         }
-        return [arg];
       });
     }
   }
   return DEFAULT_ARGUMENTS;
 };
 
-export const mergeArgumentTypes = (target, additional) => {
+export const mergeArgumentTypes = (target: Argument[][], additional: Argument[][]) => {
   for (let i = 0; i < target.length; i++) {
     if (i >= additional.length) {
       break;
     }
-    if (target[i].type === 'T') {
+    if (target[i][0].type === 'T') {
       continue;
     }
-    if (additional[i].type === 'T') {
+    if (additional[i][0].type === 'T') {
       target[i] = additional[i];
       continue;
     }
@@ -77,39 +93,37 @@ export const mergeArgumentTypes = (target, additional) => {
   }
 };
 
-export const adaptApiFunctions = functions => {
-  const udfs = [];
-  const adapted = {};
+export const adaptApiFunctions = (functions: ApiUdf[]): UdfDetails[] => {
+  const udfs: UdfDetails[] = [];
+  const adapted: { [attr: string]: UdfDetails } = {};
   functions.forEach(apiUdf => {
-    apiUdf.arguments = extractArgumentTypes(apiUdf);
-    apiUdf.returnTypes = apiUdf.return_type ? [stripPrecision(apiUdf.return_type)] : ['T'];
     if (adapted[apiUdf.name]) {
       const adaptedUdf = adapted[apiUdf.name];
-      mergeArgumentTypes(adaptedUdf.arguments, apiUdf.arguments);
+
+      const additionalArgs = extractArgumentTypes(apiUdf);
+      mergeArgumentTypes(adaptedUdf.arguments, additionalArgs);
+
       if (adaptedUdf.returnTypes[0] !== 'T') {
-        if (apiUdf.returnTypes[0] === 'T') {
-          adaptedUdf.returnTypes = ['T'];
-        } else if (adaptedUdf.returnTypes[0] !== apiUdf.returnTypes[0]) {
-          adaptedUdf.returnTypes.push(...apiUdf.returnTypes);
+        const additionalReturnTypes = extractReturnTypes(apiUdf);
+        if (additionalReturnTypes[0] !== 'T') {
+          adaptedUdf.returnTypes.push(...additionalReturnTypes);
+        } else {
+          adaptedUdf.returnTypes = additionalReturnTypes;
         }
       }
     } else {
-      adapted[apiUdf.name] = apiUdf;
-      udfs.push(apiUdf);
+      adapted[apiUdf.name] = adaptApiUdf(apiUdf);
+      udfs.push(adapted[apiUdf.name]);
     }
   });
   return udfs;
 };
 
-/**
- * @param {Object} options
- * @param {Connector} options.connector
- * @param {string} [options.database]
- * @param {boolean} [options.silenceErrors]
- *
- * @return {Promise}
- */
-export const fetchUdfs = async options => {
+export const fetchUdfs = async (options: {
+  connector: Connector;
+  database?: string;
+  silenceErrors: boolean;
+}): Promise<ApiUdf[]> => {
   let url = AUTOCOMPLETE_API_PREFIX;
   if (options.database) {
     url += '/' + options.database;

+ 2 - 2
desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.test.js → desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.test.ts

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { getArgumentDetailsForUdf } from './sqlReferenceRepository';
+import { Argument, getArgumentDetailsForUdf } from './sqlReferenceRepository';
 import * as apiUtils from 'sql/reference/apiUtils';
 
 describe('sqlReferenceRepository.js', () => {
@@ -87,7 +87,7 @@ describe('sqlReferenceRepository.js', () => {
 
   jest.spyOn(apiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));
 
-  const extractType = details => details.type;
+  const extractType = (details: Argument): string => details.type;
 
   it('should give the expected argument types at a specific position', async () => {
     expect((await getArgumentDetailsForUdf(hiveConn, 'cos', 1)).map(extractType)).toEqual([

+ 107 - 36
desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.js → desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.ts

@@ -18,26 +18,73 @@ import { matchesType } from './typeUtils';
 import I18n from 'utils/i18n';
 import huePubSub from 'utils/huePubSub';
 import { clearUdfCache, getCachedApiUdfs, setCachedApiUdfs } from './apiCache';
-import { adaptApiUdf, fetchUdfs } from './apiUtils';
+import { fetchUdfs } from './apiUtils';
+
+export interface Connector {
+  id: string;
+  dialect: string;
+}
+
+export interface Argument {
+  type: string;
+  multiple?: boolean;
+  keywords?: string[];
+  optional?: boolean;
+}
+
+export interface UdfDetails {
+  returnTypes: string[];
+  name: string;
+  arguments: Argument[][];
+  signature: string;
+  draggable: string;
+  description?: string;
+}
+
+interface UdfCategoryFunctions {
+  [attr: string]: UdfDetails;
+}
+
+interface UdfCategory {
+  name: string;
+  functions: UdfCategoryFunctions;
+  isAnalytic?: boolean;
+  isAggregate?: boolean;
+}
+
+export interface SetOptions {
+  [attr: string]: SetDetails;
+}
+
+interface SetDetails {
+  default: string;
+  type: string;
+  details: string;
+}
 
 export const CLEAR_UDF_CACHE_EVENT = 'hue.clear.udf.cache';
 
-const SET_REFS = {
+const SET_REFS: { [attr: string]: () => Promise<{ SET_OPTIONS?: SetOptions }> } = {
+  // @ts-ignore
   impala: async () => import(/* webpackChunkName: "impala-ref" */ './impala/setReference')
 };
 
-const UDF_REFS = {
+const UDF_REFS: { [attr: string]: () => Promise<{ UDF_CATEGORIES?: UdfCategory[] }> } = {
+  // @ts-ignore
   generic: async () => import(/* webpackChunkName: "generic-ref" */ './generic/udfReference'),
+  // @ts-ignore
   hive: async () => import(/* webpackChunkName: "hive-ref" */ './hive/udfReference'),
+  // @ts-ignore
   impala: async () => import(/* webpackChunkName: "impala-ref" */ './impala/udfReference'),
+  // @ts-ignore
   pig: async () => import(/* webpackChunkName: "pig-ref" */ './pig/udfReference')
 };
 
 const IGNORED_UDF_REGEX = /^[!=$%&*+-/<>^|~]+$/;
 
-const mergedUdfPromises = {};
+const mergedUdfPromises: { [attr: string]: Promise<UdfCategory[]> } = {};
 
-const getMergedUdfKey = (connector, database) => {
+const getMergedUdfKey = (connector: Connector, database?: string): string => {
   let key = connector.id;
   if (database) {
     key += '_' + database;
@@ -45,9 +92,13 @@ const getMergedUdfKey = (connector, database) => {
   return key;
 };
 
-export const hasUdfCategories = connector => typeof UDF_REFS[connector.dialect] !== 'undefined';
+export const hasUdfCategories = (connector: Connector): boolean =>
+  typeof UDF_REFS[connector.dialect] !== 'undefined';
 
-const findUdfsToAdd = (apiUdfs, existingCategories) => {
+const findUdfsToAdd = (
+  apiUdfs: UdfDetails[],
+  existingCategories: UdfCategory[]
+): UdfCategoryFunctions => {
   const existingUdfNames = new Set();
   existingCategories.forEach(category => {
     Object.keys(category.functions).forEach(udfName => {
@@ -55,7 +106,7 @@ const findUdfsToAdd = (apiUdfs, existingCategories) => {
     });
   });
 
-  const result = {};
+  const result: UdfCategoryFunctions = {};
 
   apiUdfs.forEach(apiUdf => {
     // TODO: Impala reports the same UDF multiple times, once per argument type.
@@ -64,14 +115,18 @@ const findUdfsToAdd = (apiUdfs, existingCategories) => {
       !existingUdfNames.has(apiUdf.name.toUpperCase()) &&
       !IGNORED_UDF_REGEX.test(apiUdf.name)
     ) {
-      result[apiUdf.name] = adaptApiUdf(apiUdf);
+      result[apiUdf.name] = apiUdf;
     }
   });
 
   return result;
 };
 
-const mergeWithApiUdfs = async (categories, connector, database) => {
+const mergeWithApiUdfs = async (
+  categories: UdfCategory[],
+  connector: Connector,
+  database?: string
+) => {
   let apiUdfs = await getCachedApiUdfs(connector, database);
   if (!apiUdfs) {
     apiUdfs = await fetchUdfs({
@@ -94,11 +149,14 @@ const mergeWithApiUdfs = async (categories, connector, database) => {
   }
 };
 
-export const getUdfCategories = async (connector, database) => {
+export const getUdfCategories = async (
+  connector: Connector,
+  database?: string
+): Promise<UdfCategory[]> => {
   const promiseKey = getMergedUdfKey(connector, database);
   if (!mergedUdfPromises[promiseKey]) {
     mergedUdfPromises[promiseKey] = new Promise(async resolve => {
-      let categories = [];
+      let categories: UdfCategory[] = [];
       if (UDF_REFS[connector.dialect]) {
         const module = await UDF_REFS[connector.dialect]();
         if (module.UDF_CATEGORIES) {
@@ -118,9 +176,12 @@ export const getUdfCategories = async (connector, database) => {
   return await mergedUdfPromises[promiseKey];
 };
 
-export const findUdf = async (connector, functionName) => {
+export const findUdf = async (
+  connector: Connector,
+  functionName: string
+): Promise<UdfDetails[]> => {
   const categories = await getUdfCategories(connector);
-  const found = [];
+  const found: UdfDetails[] = [];
   categories.forEach(category => {
     if (category.functions[functionName]) {
       found.push(category.functions[functionName]);
@@ -129,14 +190,17 @@ export const findUdf = async (connector, functionName) => {
   return found;
 };
 
-export const getReturnTypesForUdf = async (connector, functionName) => {
+export const getReturnTypesForUdf = async (
+  connector: Connector,
+  functionName: string
+): Promise<string[]> => {
   if (!functionName) {
     return ['T'];
   }
   const udfs = await findUdf(connector, functionName);
   if (!udfs.length) {
     let returnTypesPresent = false;
-    const returnTypes = new Set();
+    const returnTypes = new Set<string>();
     udfs.forEach(udf => {
       if (udf.returnTypes) {
         returnTypesPresent = true;
@@ -144,7 +208,7 @@ export const getReturnTypesForUdf = async (connector, functionName) => {
       }
     });
     if (returnTypesPresent) {
-      return Array.from(returnTypes.entries());
+      return [...returnTypes];
     }
   }
 
@@ -152,13 +216,13 @@ export const getReturnTypesForUdf = async (connector, functionName) => {
 };
 
 export const getUdfsWithReturnTypes = async (
-  connector,
-  returnTypes,
-  includeAggregate,
-  includeAnalytic
-) => {
+  connector: Connector,
+  returnTypes: string[],
+  includeAggregate?: boolean,
+  includeAnalytic?: boolean
+): Promise<UdfDetails[]> => {
   const categories = await getUdfCategories(connector);
-  const result = [];
+  const result: UdfDetails[] = [];
   categories.forEach(category => {
     if (
       (!category.isAnalytic && !category.isAggregate) ||
@@ -167,7 +231,7 @@ export const getUdfsWithReturnTypes = async (
     ) {
       Object.keys(category.functions).forEach(udfName => {
         const udf = category.functions[udfName];
-        if (!returnTypes || matchesType(connector, returnTypes, udf.returnTypes)) {
+        if (!returnTypes || matchesType(connector.dialect, returnTypes, udf.returnTypes)) {
           result.push(udf);
         }
       });
@@ -177,13 +241,17 @@ export const getUdfsWithReturnTypes = async (
   return result;
 };
 
-export const getArgumentDetailsForUdf = async (connector, functionName, argumentPosition) => {
+export const getArgumentDetailsForUdf = async (
+  connector: Connector,
+  functionName: string,
+  argumentPosition: number
+): Promise<Argument[]> => {
   const foundFunctions = await findUdf(connector, functionName);
   if (!foundFunctions.length) {
     return [{ type: 'T' }];
   }
 
-  const possibleArguments = [];
+  const possibleArguments: Argument[] = [];
   foundFunctions.forEach(foundFunction => {
     const args = foundFunction.arguments;
     if (argumentPosition > args.length) {
@@ -195,7 +263,7 @@ export const getArgumentDetailsForUdf = async (connector, functionName, argument
   return possibleArguments;
 };
 
-export const getSetOptions = async connector => {
+export const getSetOptions = async (connector: Connector): Promise<SetOptions> => {
   if (SET_REFS[connector.dialect]) {
     const module = await SET_REFS[connector.dialect]();
     if (module.SET_OPTIONS) {
@@ -205,14 +273,17 @@ export const getSetOptions = async connector => {
   return {};
 };
 
-huePubSub.subscribe(CLEAR_UDF_CACHE_EVENT, async details => {
-  await clearUdfCache(details.connector);
-  Object.keys(mergedUdfPromises).forEach(key => {
-    if (key === details.connector.id || key.indexOf(details.connector.id + '_') === 0) {
-      delete mergedUdfPromises[key];
+huePubSub.subscribe(
+  CLEAR_UDF_CACHE_EVENT,
+  async (details: { connector: Connector; callback: () => void }) => {
+    await clearUdfCache(details.connector);
+    Object.keys(mergedUdfPromises).forEach(key => {
+      if (key === details.connector.id || key.indexOf(details.connector.id + '_') === 0) {
+        delete mergedUdfPromises[key];
+      }
+    });
+    if (details.callback) {
+      details.callback();
     }
-  });
-  if (details.callback) {
-    details.callback();
   }
-});
+);

+ 2 - 2
desktop/core/src/desktop/js/sql/reference/typeUtils.test.js → desktop/core/src/desktop/js/sql/reference/typeUtils.test.ts

@@ -14,9 +14,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { matchesType } from 'sql/reference/typeUtils';
+import { matchesType } from './typeUtils';
 
-describe('typeUtils.js', () => {
+describe('typeUtils.ts', () => {
   it('should matchTypes for NUMBER', () => {
     expect(matchesType('hive', ['NUMBER'], ['INT'])).toBeTruthy();
     expect(matchesType('hive', ['NUMBER'], ['BIGINT'])).toBeTruthy();

+ 12 - 9
desktop/core/src/desktop/js/sql/reference/typeUtils.js → desktop/core/src/desktop/js/sql/reference/typeUtils.ts

@@ -18,8 +18,12 @@ import { TYPE_CONVERSION as HIVE_TYPE_CONVERSION } from './hive/typeConversion';
 import { TYPE_CONVERSION as IMPALA_TYPE_CONVERSION } from './impala/typeConversion';
 import { TYPE_CONVERSION as GENERIC_TYPE_CONVERSION } from './generic/typeConversion';
 
-const stripPrecision = function(types) {
-  const result = [];
+export interface TypeConversion {
+  [attr: string]: { [attr: string]: boolean };
+}
+
+const stripPrecision = (types: string[]): string[] => {
+  const result: string[] = [];
   types.forEach(type => {
     if (type.indexOf('(') > -1) {
       result.push(type.substring(0, type.indexOf('(')));
@@ -30,7 +34,7 @@ const stripPrecision = function(types) {
   return result;
 };
 
-const getTypeConversion = dialect => {
+const getTypeConversion = (dialect: string): TypeConversion => {
   if (dialect === 'impala') {
     return IMPALA_TYPE_CONVERSION;
   }
@@ -42,13 +46,12 @@ const getTypeConversion = dialect => {
 
 /**
  * Matches types based on implicit conversion i.e. if you expect a BIGINT then INT is ok but not BOOLEAN etc.
- *
- * @param dialect
- * @param expectedTypes
- * @param actualRawTypes
- * @returns {boolean}
  */
-export const matchesType = function(dialect, expectedTypes, actualRawTypes) {
+export const matchesType = (
+  dialect: string,
+  expectedTypes: string[],
+  actualRawTypes: string[]
+): boolean => {
   if (expectedTypes.length === 1 && expectedTypes[0] === 'T') {
     return true;
   }