From 1ef646884c5ffa718d569baa529d2b8e234c5f1c Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 14:21:44 +0200 Subject: [PATCH 1/9] Extract formatDialect() from format() --- src/sqlFormatter.ts | 50 ++++++++++++++++++++++++++------------- test/sqlFormatter.test.ts | 16 +++++++------ 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index f9b34d1344..096aeb1407 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -42,12 +42,15 @@ export const formatters = { export type SqlLanguage = keyof typeof formatters; export const supportedDialects = Object.keys(formatters); -export interface FormatOptionsWithLanguage extends FormatOptions { - language: SqlLanguage | DialectOptions; -} +export type FormatOptionsWithLanguage = Partial & { + language?: SqlLanguage; +}; + +export type FormatOptionsWithDialect = Partial & { + dialect: DialectOptions; +}; -const defaultOptions: FormatOptionsWithLanguage = { - language: 'sql', +const defaultOptions: FormatOptions = { tabWidth: 2, useTabs: false, keywordCase: 'preserve', @@ -65,10 +68,32 @@ const defaultOptions: FormatOptionsWithLanguage = { * Format whitespace in a query to make it easier to read. * * @param {string} query - input SQL query string - * @param {Partial} cfg Configuration options (see docs in README) + * @param {FormatOptionsWithLanguage} cfg Configuration options (see docs in README) + * @return {string} formatted query + */ +export const format = (query: string, cfg: FormatOptionsWithLanguage = {}): string => { + if (typeof cfg.language === 'string' && !supportedDialects.includes(cfg.language)) { + throw new ConfigError(`Unsupported SQL dialect: ${cfg.language}`); + } + + return formatDialect(query, { + ...cfg, + dialect: formatters[cfg.language || 'sql'], + }); +}; + +/** + * Like the above format(), but language parameter is mandatory + * and must be a Dialect object instead of a string. + * + * @param {string} query - input SQL query string + * @param {FormatOptionsWithDialect} cfg Configuration options (see docs in README) * @return {string} formatted query */ -export const format = (query: string, cfg: Partial = {}): string => { +export const formatDialect = ( + query: string, + { dialect, ...cfg }: FormatOptionsWithDialect +): string => { if (typeof query !== 'string') { throw new Error('Invalid query argument. Expected string, instead got ' + typeof query); } @@ -78,19 +103,12 @@ export const format = (query: string, cfg: Partial = ...cfg, }); - const dialectOptions: DialectOptions = - typeof options.language === 'string' ? formatters[options.language] : options.language; - - return new Formatter(createDialect(dialectOptions), options).format(query); + return new Formatter(createDialect(dialect), options).format(query); }; export class ConfigError extends Error {} -function validateConfig(cfg: FormatOptionsWithLanguage): FormatOptionsWithLanguage { - if (typeof cfg.language === 'string' && !supportedDialects.includes(cfg.language)) { - throw new ConfigError(`Unsupported SQL dialect: ${cfg.language}`); - } - +function validateConfig(cfg: FormatOptions): FormatOptions { if ('multilineLists' in cfg) { throw new ConfigError('multilineLists config is no more supported.'); } diff --git a/test/sqlFormatter.test.ts b/test/sqlFormatter.test.ts index a6e5ddd861..7cbd4983e3 100644 --- a/test/sqlFormatter.test.ts +++ b/test/sqlFormatter.test.ts @@ -1,6 +1,6 @@ import dedent from 'dedent-js'; -import { format, SqlLanguage } from '../src/sqlFormatter.js'; +import { format, formatDialect, SqlLanguage } from '../src/sqlFormatter.js'; import { sqlite } from '../src/languages/sqlite/sqlite.formatter.js'; describe('sqlFormatter', () => { @@ -56,11 +56,13 @@ describe('sqlFormatter', () => { }).toThrow('aliasAs config is no more supported.'); }); - it('allows passing Dialect config object as a language parameter', () => { - expect(format('SELECT [foo], `bar`;', { language: sqlite })).toBe(dedent` - SELECT - [foo], - \`bar\`; - `); + describe('formatDialect()', () => { + it('allows passing Dialect config object as a dialect parameter', () => { + expect(formatDialect('SELECT [foo], `bar`;', { dialect: sqlite })).toBe(dedent` + SELECT + [foo], + \`bar\`; + `); + }); }); }); From c649a812781632afdfe6d17671995afdc8a76a8c Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 14:33:27 +0200 Subject: [PATCH 2/9] Use explicit exports from sqlFormatter.ts Don't just re-export all, as that will easily lead to unwanted exports, like the export of formatters map, which should not have been publicly exposed. --- src/index.ts | 10 +++++++++- src/sqlFormatter.ts | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index ed35822556..61916edeb5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,12 @@ -export * from './sqlFormatter.js'; +export { + SqlLanguage, + supportedDialects, + FormatOptionsWithLanguage, + FormatOptionsWithDialect, + format, + formatDialect, + ConfigError, +} from './sqlFormatter.js'; export type { IndentStyle, KeywordCase, diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index 096aeb1407..d660ab0aad 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -20,7 +20,7 @@ import { ParamItems } from './formatter/Params.js'; import { createDialect, DialectOptions } from './dialect.js'; import Formatter from './formatter/Formatter.js'; -export const formatters = { +const formatters = { bigquery, db2, hive, From 9eb017c779a00edd4f559f16ac8e4190fdda0304 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 14:40:04 +0200 Subject: [PATCH 3/9] Extract validateConfig() to separate file --- src/index.ts | 2 +- src/sqlFormatter.ts | 43 +------------------------------------------ src/validateConfig.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 43 deletions(-) create mode 100644 src/validateConfig.ts diff --git a/src/index.ts b/src/index.ts index 61916edeb5..b741bd6895 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ export { FormatOptionsWithDialect, format, formatDialect, - ConfigError, } from './sqlFormatter.js'; export type { IndentStyle, @@ -15,4 +14,5 @@ export type { FormatOptions, } from './FormatOptions.js'; export type { DialectOptions } from './dialect.js'; +export { ConfigError } from './validateConfig.js'; export { expandPhrases } from './expandPhrases.js'; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index d660ab0aad..55d5e79195 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -16,9 +16,9 @@ import { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter import { snowflake } from './languages/snowflake/snowflake.formatter.js'; import { FormatOptions } from './FormatOptions.js'; -import { ParamItems } from './formatter/Params.js'; import { createDialect, DialectOptions } from './dialect.js'; import Formatter from './formatter/Formatter.js'; +import { ConfigError, validateConfig } from './validateConfig.js'; const formatters = { bigquery, @@ -106,45 +106,4 @@ export const formatDialect = ( return new Formatter(createDialect(dialect), options).format(query); }; -export class ConfigError extends Error {} - -function validateConfig(cfg: FormatOptions): FormatOptions { - if ('multilineLists' in cfg) { - throw new ConfigError('multilineLists config is no more supported.'); - } - if ('newlineBeforeOpenParen' in cfg) { - throw new ConfigError('newlineBeforeOpenParen config is no more supported.'); - } - if ('newlineBeforeCloseParen' in cfg) { - throw new ConfigError('newlineBeforeCloseParen config is no more supported.'); - } - if ('aliasAs' in cfg) { - throw new ConfigError('aliasAs config is no more supported.'); - } - - if (cfg.expressionWidth <= 0) { - throw new ConfigError( - `expressionWidth config must be positive number. Received ${cfg.expressionWidth} instead.` - ); - } - - if (cfg.commaPosition === 'before' && cfg.useTabs) { - throw new ConfigError( - 'commaPosition: before does not work when tabs are used for indentation.' - ); - } - - if (cfg.params && !validateParams(cfg.params)) { - // eslint-disable-next-line no-console - console.warn('WARNING: All "params" option values should be strings.'); - } - - return cfg; -} - -function validateParams(params: ParamItems | string[]): boolean { - const paramValues = params instanceof Array ? params : Object.values(params); - return paramValues.every(p => typeof p === 'string'); -} - export type FormatFn = typeof format; diff --git a/src/validateConfig.ts b/src/validateConfig.ts new file mode 100644 index 0000000000..caa4efbfeb --- /dev/null +++ b/src/validateConfig.ts @@ -0,0 +1,43 @@ +import { FormatOptions } from './FormatOptions.js'; +import { ParamItems } from './formatter/Params.js'; + +export class ConfigError extends Error {} + +export function validateConfig(cfg: FormatOptions): FormatOptions { + if ('multilineLists' in cfg) { + throw new ConfigError('multilineLists config is no more supported.'); + } + if ('newlineBeforeOpenParen' in cfg) { + throw new ConfigError('newlineBeforeOpenParen config is no more supported.'); + } + if ('newlineBeforeCloseParen' in cfg) { + throw new ConfigError('newlineBeforeCloseParen config is no more supported.'); + } + if ('aliasAs' in cfg) { + throw new ConfigError('aliasAs config is no more supported.'); + } + + if (cfg.expressionWidth <= 0) { + throw new ConfigError( + `expressionWidth config must be positive number. Received ${cfg.expressionWidth} instead.` + ); + } + + if (cfg.commaPosition === 'before' && cfg.useTabs) { + throw new ConfigError( + 'commaPosition: before does not work when tabs are used for indentation.' + ); + } + + if (cfg.params && !validateParams(cfg.params)) { + // eslint-disable-next-line no-console + console.warn('WARNING: All "params" option values should be strings.'); + } + + return cfg; +} + +function validateParams(params: ParamItems | string[]): boolean { + const paramValues = params instanceof Array ? params : Object.values(params); + return paramValues.every(p => typeof p === 'string'); +} From 829e98f3f397621d0e40e9b915393fc7d674ffa5 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 14:43:57 +0200 Subject: [PATCH 4/9] Gather all dialects imports/exports to separate file --- src/allDialects.ts | 16 ++++++++++++++++ src/sqlFormatter.ts | 36 +++--------------------------------- 2 files changed, 19 insertions(+), 33 deletions(-) create mode 100644 src/allDialects.ts diff --git a/src/allDialects.ts b/src/allDialects.ts new file mode 100644 index 0000000000..7ba24ffb1e --- /dev/null +++ b/src/allDialects.ts @@ -0,0 +1,16 @@ +export { bigquery } from './languages/bigquery/bigquery.formatter.js'; +export { db2 } from './languages/db2/db2.formatter.js'; +export { hive } from './languages/hive/hive.formatter.js'; +export { mariadb } from './languages/mariadb/mariadb.formatter.js'; +export { mysql } from './languages/mysql/mysql.formatter.js'; +export { n1ql } from './languages/n1ql/n1ql.formatter.js'; +export { plsql } from './languages/plsql/plsql.formatter.js'; +export { postgresql } from './languages/postgresql/postgresql.formatter.js'; +export { redshift } from './languages/redshift/redshift.formatter.js'; +export { spark } from './languages/spark/spark.formatter.js'; +export { sqlite } from './languages/sqlite/sqlite.formatter.js'; +export { sql } from './languages/sql/sql.formatter.js'; +export { trino } from './languages/trino/trino.formatter.js'; +export { transactsql } from './languages/transactsql/transactsql.formatter.js'; +export { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js'; +export { snowflake } from './languages/snowflake/snowflake.formatter.js'; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index 55d5e79195..abee1b2479 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -1,19 +1,4 @@ -import { bigquery } from './languages/bigquery/bigquery.formatter.js'; -import { db2 } from './languages/db2/db2.formatter.js'; -import { hive } from './languages/hive/hive.formatter.js'; -import { mariadb } from './languages/mariadb/mariadb.formatter.js'; -import { mysql } from './languages/mysql/mysql.formatter.js'; -import { n1ql } from './languages/n1ql/n1ql.formatter.js'; -import { plsql } from './languages/plsql/plsql.formatter.js'; -import { postgresql } from './languages/postgresql/postgresql.formatter.js'; -import { redshift } from './languages/redshift/redshift.formatter.js'; -import { spark } from './languages/spark/spark.formatter.js'; -import { sqlite } from './languages/sqlite/sqlite.formatter.js'; -import { sql } from './languages/sql/sql.formatter.js'; -import { trino } from './languages/trino/trino.formatter.js'; -import { transactsql } from './languages/transactsql/transactsql.formatter.js'; -import { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js'; -import { snowflake } from './languages/snowflake/snowflake.formatter.js'; +import * as allDialects from './allDialects.js'; import { FormatOptions } from './FormatOptions.js'; import { createDialect, DialectOptions } from './dialect.js'; @@ -21,23 +6,8 @@ import Formatter from './formatter/Formatter.js'; import { ConfigError, validateConfig } from './validateConfig.js'; const formatters = { - bigquery, - db2, - hive, - mariadb, - mysql, - n1ql, - plsql, - postgresql, - redshift, - singlestoredb, - snowflake, - spark, - sql, - sqlite, - transactsql, - trino, - tsql: transactsql, // alias for transactsql + ...allDialects, + tsql: allDialects.transactsql, // alias for transactsql }; export type SqlLanguage = keyof typeof formatters; export const supportedDialects = Object.keys(formatters); From 4866fb80f7e7200a8cbfabd56c5f3a4748d92668 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 14:45:32 +0200 Subject: [PATCH 5/9] Export all dialects through public API --- src/index.ts | 1 + test/sqlFormatter.test.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index b741bd6895..ca29696ab2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,3 +16,4 @@ export type { export type { DialectOptions } from './dialect.js'; export { ConfigError } from './validateConfig.js'; export { expandPhrases } from './expandPhrases.js'; +export * from './allDialects.js'; diff --git a/test/sqlFormatter.test.ts b/test/sqlFormatter.test.ts index 7cbd4983e3..dabafa2ef0 100644 --- a/test/sqlFormatter.test.ts +++ b/test/sqlFormatter.test.ts @@ -1,7 +1,6 @@ import dedent from 'dedent-js'; -import { format, formatDialect, SqlLanguage } from '../src/sqlFormatter.js'; -import { sqlite } from '../src/languages/sqlite/sqlite.formatter.js'; +import { format, formatDialect, SqlLanguage, sqlite } from '../src/index.js'; describe('sqlFormatter', () => { it('throws error when unsupported language parameter specified', () => { From f8cf3eb0837ae3134e31fe9fb0e999009d4e45e3 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 14:46:17 +0200 Subject: [PATCH 6/9] Remove type exports in index.ts --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index ca29696ab2..723a658416 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,14 +6,14 @@ export { format, formatDialect, } from './sqlFormatter.js'; -export type { +export { IndentStyle, KeywordCase, CommaPosition, LogicalOperatorNewline, FormatOptions, } from './FormatOptions.js'; -export type { DialectOptions } from './dialect.js'; +export { DialectOptions } from './dialect.js'; export { ConfigError } from './validateConfig.js'; export { expandPhrases } from './expandPhrases.js'; export * from './allDialects.js'; From 51ca9ed20659583db05a270fef188820ed577397 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 15:03:28 +0200 Subject: [PATCH 7/9] Use export type to satisfy webpack Also add comment to avoid removing this syntax in the future --- src/index.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 723a658416..abf18a76f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,20 @@ -export { +export { supportedDialects, format, formatDialect } from './sqlFormatter.js'; +export { expandPhrases } from './expandPhrases.js'; +export * from './allDialects.js'; + +// NB! To re-export types the "export type" syntax is required by webpack. +// Otherwise webpack build will fail. +export type { SqlLanguage, - supportedDialects, FormatOptionsWithLanguage, FormatOptionsWithDialect, - format, - formatDialect, } from './sqlFormatter.js'; -export { +export type { IndentStyle, KeywordCase, CommaPosition, LogicalOperatorNewline, FormatOptions, } from './FormatOptions.js'; -export { DialectOptions } from './dialect.js'; -export { ConfigError } from './validateConfig.js'; -export { expandPhrases } from './expandPhrases.js'; -export * from './allDialects.js'; +export type { DialectOptions } from './dialect.js'; +export type { ConfigError } from './validateConfig.js'; From 6329fad24c0dcfa3d823632441f9e071bc2c427c Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Wed, 2 Nov 2022 15:05:46 +0200 Subject: [PATCH 8/9] Clarify usage of export * --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index abf18a76f9..76b5773264 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,8 @@ export { supportedDialects, format, formatDialect } from './sqlFormatter.js'; export { expandPhrases } from './expandPhrases.js'; + +// Intentionally use "export *" syntax here to make sure when adding a new SQL dialect +// we wouldn't forget to expose it in our public API. export * from './allDialects.js'; // NB! To re-export types the "export type" syntax is required by webpack. From ceeef7107f6de201ad3257cf9d5300813b9feb81 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Fri, 4 Nov 2022 18:58:12 +0200 Subject: [PATCH 9/9] Add consistent-type-exports rule to ESLint --- .eslintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 72910350d7..c7932b6939 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,7 +45,8 @@ { "avoidEscape": true, "allowTemplateLiterals": true } ], "@typescript-eslint/semi": "error", - "@typescript-eslint/no-non-null-assertion": "error" + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/consistent-type-exports": "error" }, "settings": { "import/resolver": {