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": { 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/index.ts b/src/index.ts index ed35822556..76b5773264 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,17 @@ -export * from './sqlFormatter.js'; +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. +// Otherwise webpack build will fail. +export type { + SqlLanguage, + FormatOptionsWithLanguage, + FormatOptionsWithDialect, +} from './sqlFormatter.js'; export type { IndentStyle, KeywordCase, @@ -7,4 +20,4 @@ export type { FormatOptions, } from './FormatOptions.js'; export type { DialectOptions } from './dialect.js'; -export { expandPhrases } from './expandPhrases.js'; +export type { ConfigError } from './validateConfig.js'; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index f9b34d1344..abee1b2479 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -1,53 +1,26 @@ -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 { ParamItems } from './formatter/Params.js'; import { createDialect, DialectOptions } from './dialect.js'; import Formatter from './formatter/Formatter.js'; +import { ConfigError, validateConfig } from './validateConfig.js'; -export const formatters = { - bigquery, - db2, - hive, - mariadb, - mysql, - n1ql, - plsql, - postgresql, - redshift, - singlestoredb, - snowflake, - spark, - sql, - sqlite, - transactsql, - trino, - tsql: transactsql, // alias for transactsql +const formatters = { + ...allDialects, + tsql: allDialects.transactsql, // alias for transactsql }; 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 +38,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,55 +73,7 @@ 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}`); - } - - 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'); +} diff --git a/test/sqlFormatter.test.ts b/test/sqlFormatter.test.ts index a6e5ddd861..dabafa2ef0 100644 --- a/test/sqlFormatter.test.ts +++ b/test/sqlFormatter.test.ts @@ -1,7 +1,6 @@ import dedent from 'dedent-js'; -import { format, 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', () => { @@ -56,11 +55,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\`; + `); + }); }); });