diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js new file mode 100644 index 0000000..b4a0e28 --- /dev/null +++ b/spec/SqlSynonym.spec.js @@ -0,0 +1,57 @@ +import {OpenDataQueryFormatter, QueryExpression, SqlFormatter, SqlSynonym} from '../src/index'; + +describe('SqlSynonym', () => { + const synonyms = SqlSynonym.getInstance(); + + beforeEach(() => { + synonyms.clear(); + }); + + afterAll(() => { + synonyms.clear(); + }); + + it('should format query by using SQL synonym', () => { + synonyms.set('Products', 'sales.Products'); + const formatter = new SqlFormatter(); + formatter.settings.nameFormat = '`$1`'; + const query = new QueryExpression().select('id', 'name') + .from('Products').where('id').equal(100); + const sql = formatter.formatSelect(query); + const expectedSql = 'SELECT `sales`.`Products`.`id`, `sales`.`Products`.`name` FROM `sales`.`Products` WHERE (`id`=100)'; + expect(sql).toBe(expectedSql); + }); + + it('should format qualified object names by using synonym', () => { + synonyms.set('Products', 'Production.Product'); + const formatter = new SqlFormatter(); + formatter.settings.nameFormat = '`$1`'; + expect(formatter.escapeEntity('Products')).toBe('`Production`.`Product`'); + expect(formatter.escapeName('Products.ProductID')).toBe('`Production`.`Product`.`ProductID`'); + }); + + it('should apply synonyms in formatter subclasses', () => { + synonyms.set('Products', 'sales.Products'); + const formatter = new OpenDataQueryFormatter(); + expect(formatter.escapeName('Products.id')).toBe('sales/Products/id'); + }); + + it('should prioritize formatter instance synonyms over static resolving handlers', () => { + synonyms.set('Products', 'sales.Products'); + const formatter = new SqlFormatter(); + formatter.settings.nameFormat = '`$1`'; + formatter.synonyms = new SqlSynonym([ + ['Products', 'inventory.Products'] + ]); + expect(formatter.escapeEntity('Products')).toBe('`inventory`.`Products`'); + }); + + it('should construct synonyms from array entries', () => { + const localSynonyms = new SqlSynonym([ + ['synonym1', 'schema1.object1'], + ['synonym2', 'schema2.object2'] + ]); + expect(localSynonyms.get('synonym1')).toBe('schema1.object1'); + expect(localSynonyms.get('synonym2')).toBe('schema2.object2'); + }); +}); diff --git a/src/formatter.d.ts b/src/formatter.d.ts index 520b50a..5d2b0a6 100644 --- a/src/formatter.d.ts +++ b/src/formatter.d.ts @@ -1,5 +1,6 @@ // MOST Web Framework Codename Zero Gravity Copyright (c) 2017-2022, THEMOST LP All rights reserved import {QueryEntity, QueryExpression, QueryField, QueryValueRef} from './query'; +import {SyncSeriesEventEmitter} from '@themost/events'; export declare interface FormatterSettings { nameFormat: string; @@ -10,7 +11,10 @@ export declare interface FormatterSettings { export type QueryToken = string | any; export declare class SqlFormatter { + static resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string }>; provider: any; + synonyms: { resolve(name: string): string } | null; + resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string }>; settings: FormatterSettings; escape(value: any,unquoted?: boolean): string | any; diff --git a/src/formatter.js b/src/formatter.js index 3f6ac63..aecc8f0 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -14,6 +14,7 @@ import { ObjectNameValidator } from './object-name.validator'; import {isMethodOrNameReference, isNameReference, trimNameReference} from './name-reference'; import { JSONArray, JSONObject } from '@themost/json'; import {MethodCallExpression} from './expressions'; +import {SyncSeriesEventEmitter} from '@themost/events'; class AbstractMethodError extends Error { constructor() { @@ -36,6 +37,17 @@ function getAliasKeyword() { return ' '; } +function resolveName(formatter, name) { + let str = name; + if (isNameReference(str)) { + str = trimNameReference(name); + } + if (formatter.synonyms && typeof formatter.synonyms.resolve === 'function') { + str = formatter.synonyms.resolve(str); + } + return str; +} + /** * Initializes an SQL formatter class. * @class SqlFormatter @@ -45,6 +57,8 @@ class SqlFormatter { constructor() { // this.provider = null; + this.synonyms = null; + this.resolvingName = SqlFormatter.resolvingName; /** * Gets or sets formatter settings * @type {{nameFormat: string, forceAlias: boolean, useAliasKeyword: boolean}|*} @@ -1093,22 +1107,24 @@ class SqlFormatter { if (typeof name !== 'string') { throw new Error('Invalid name expression. Expected string.'); } - let str = name; - if (isNameReference(str)) { - str = trimNameReference(name); - } - return ObjectNameValidator.validator.escape(str, this.settings.nameFormat); + const event = { + target: this, + name: resolveName(this, name) + }; + this.resolvingName.emit(event); + return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); } escapeEntity(name) { if (typeof name !== 'string') { throw new Error('Invalid entity expression. Expected string.'); } - let str = name; - if (isNameReference(str)) { - str = trimNameReference(name); - } - return ObjectNameValidator.validator.escape(str, this.settings.nameFormat); + const event = { + target: this, + name: resolveName(this, name) + }; + this.resolvingName.emit(event); + return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); } /** @@ -1638,6 +1654,8 @@ class SqlFormatter { } +SqlFormatter.resolvingName = new SyncSeriesEventEmitter(); + export { SqlFormatter } diff --git a/src/index.d.ts b/src/index.d.ts index adb4f93..2f635aa 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -13,4 +13,4 @@ export * from './closures/StringMethodParser'; export * from './object-name.validator'; export * from './open-data-query.expression'; export * from './open-data-query.formatter'; - +export * from './sql-synonym'; diff --git a/src/index.js b/src/index.js index 64c1c03..2f635aa 100644 --- a/src/index.js +++ b/src/index.js @@ -12,4 +12,5 @@ export * from './closures/DateMethodParser'; export * from './closures/StringMethodParser'; export * from './object-name.validator'; export * from './open-data-query.expression'; -export * from './open-data-query.formatter'; \ No newline at end of file +export * from './open-data-query.formatter'; +export * from './sql-synonym'; diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts new file mode 100644 index 0000000..597f051 --- /dev/null +++ b/src/sql-synonym.d.ts @@ -0,0 +1,6 @@ +export declare class SqlSynonym extends Map { + constructor(); + constructor(entries?: readonly (readonly [string, string])[] | null); + resolve(name: string): string; + static getInstance(): SqlSynonym; +} diff --git a/src/sql-synonym.js b/src/sql-synonym.js new file mode 100644 index 0000000..b370540 --- /dev/null +++ b/src/sql-synonym.js @@ -0,0 +1,72 @@ +import {SqlFormatter} from './formatter'; + +class SqlSynonym extends Map { + constructor(entries) { + super(entries); + } + + set(synonym, name) { + if (typeof synonym !== 'string') { + throw new TypeError('Invalid synonym name. Expected string.'); + } + if (typeof name !== 'string') { + throw new TypeError('Invalid target name. Expected string.'); + } + return super.set(synonym, name); + } + + get(synonym) { + return super.get(synonym); + } + + delete(synonym) { + if (typeof synonym !== 'string') { + throw new TypeError('Invalid synonym name. Expected string.'); + } + return super.delete(synonym); + } + + clear() { + return super.clear(); + } + + resolve(name) { + if (typeof name !== 'string') { + throw new TypeError('Invalid synonym expression. Expected string.'); + } + const exactMatch = this.get(name); + if (typeof exactMatch === 'string') { + return exactMatch; + } + let candidate; + for (const key of this.keys()) { + if (name.startsWith(key + '.')) { + if (candidate === undefined || key.length > candidate.length) { + candidate = key; + } + } + } + if (typeof candidate === 'string') { + return this.get(candidate).concat(name.substring(candidate.length)); + } + return name; + } + + static getInstance() { + if (this.instance === undefined) { + this.instance = new SqlSynonym(); + } + return this.instance; + } +} + +SqlFormatter.resolvingName.subscribe((event) => { + if (typeof event.name !== 'string') { + return; + } + event.name = SqlSynonym.getInstance().resolve(event.name); +}); + +export { + SqlSynonym +};