Skip to content

Commit

Permalink
Set object name validator event (#85)
Browse files Browse the repository at this point in the history
* provide a new function for escaping an entity

* add validating event

* add codacy config

* update codacy config

* remove eslint comment

* 2.11.4
  • Loading branch information
kbarbounakis authored Apr 9, 2024
1 parent 30bde57 commit 10072bf
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 24 deletions.
10 changes: 10 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
engines:
duplication:
exclude_paths:
- "docs/**"
- "spec/**/*"
exclude_paths:
- "docs/**"
- "spec/**/*"
- ".github/**/*"
- "babel.config.js"
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ tsconfig.json

# gitpod
.gitpod.yml

# codacy
.codacy.yml
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@themost/query",
"version": "2.11.3",
"version": "2.11.4",
"description": "MOST Web Framework Codename ZeroGravity - Query Module",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
Expand Down
121 changes: 121 additions & 0 deletions spec/SimpleQueryFormatter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {SqlFormatter, QueryExpression, ObjectNameValidator, QueryEntity, QueryField} from '../src/index';

const TRIM_QUALIFIED_NAME_REGEXP = /^(\w+)((\.(\w+))+)/;
const TRIM_SAME_ALIAS_REGEXP = /^(.*)\sAS\s(.*)$/;

class SimpleSqlFormatter extends SqlFormatter {
// _validator = new SimpleObjectNameValidator();
constructor(options) {
super();
this.settings = Object.assign({
nameFormat: '$1',
forceAlias: false
}, options);
}
// get validator() {
// return this._validator;
// }

/**
* @param {*} name
* @returns {string}
*/
escapeName(name) {
if (typeof name === 'object' && Object.prototype.hasOwnProperty.call(name, '$name')) {
return this.escapeName(name.$name);
}
if (typeof name !== 'string') {
throw new Error('Invalid name expression. Expected string.');
}
const matches = TRIM_QUALIFIED_NAME_REGEXP.exec(name);
if (matches) {
return this.escapeName(matches[matches.length - 1]);
}
return super.escapeName(name);
}

escapeEntity(name) {
return super.escapeEntity(name);
}

formatFieldEx(field, format) {
if (Object.prototype.hasOwnProperty.call(field, '$name')) {
const { $name: name } = field;
const matches = TRIM_QUALIFIED_NAME_REGEXP.exec(name);
if (matches) {
return this.escapeName(matches[matches.length - 1]);
}
}
const result = super.formatFieldEx(field, format);
const matches = TRIM_SAME_ALIAS_REGEXP.exec(result);
if (matches && matches[1] === matches[2]) {
return matches[1];
}
return result;
}
}


describe('SimpleQueryFormatter', () => {
const onValidateName = (event) => {
// validate database object name by allowing qualified names e.g. dbo.Products
event.valid = ObjectNameValidator.validator.test(event.name, true);
};
beforeAll(() => {
ObjectNameValidator.validator.validating.subscribe(onValidateName);
});
afterAll(() => {
//
ObjectNameValidator.validator.validating.unsubscribe(onValidateName);
});
it('should create a simple select expression', () => {
const query = new QueryExpression().select('id', 'name', 'category').from('ProductData');
const formatter = new SimpleSqlFormatter();
const sql = formatter.formatSelect(query);
expect(sql).toBe('SELECT id, name, category FROM ProductData');
});
it('should select from table with qualified name', () => {

const query = new QueryExpression().select('id', 'name', 'category').from('dbo.ProductData');
const formatter = new SimpleSqlFormatter();
const sql = formatter.formatSelect(query);
expect(sql).toBe('SELECT id, name, category FROM dbo.ProductData');
});

it('should select from entity with qualified name', () => {

let query = new QueryExpression().select('id', 'name', 'category').from(new QueryEntity('dbo.ProductData'));
const formatter = new SimpleSqlFormatter();
let sql = formatter.formatSelect(query);
expect(sql).toBe('SELECT id, name, category FROM dbo.ProductData');
query = new QueryExpression().select(
new QueryField('id'),
new QueryField('name'),
new QueryField('category')
).from(new QueryEntity('dbo.ProductData'));
sql = formatter.formatSelect(query);
expect(sql).toBe('SELECT id, name, category FROM dbo.ProductData');

});

it('should select by using query fields', () => {
const formatter = new SimpleSqlFormatter();
const query = new QueryExpression().select(
new QueryField('id'),
new QueryField('name'),
new QueryField('category')
).from(new QueryEntity('dbo.ProductData'));
const sql = formatter.formatSelect(query);
expect(sql).toBe('SELECT id, name, category FROM dbo.ProductData');
});

it('should select by using closures', () => {
const formatter = new SimpleSqlFormatter();
const Products = new QueryEntity('dbo.Products');
const query = new QueryExpression().select(({id, name, category}) => {
return {id, name, category};
}).from(Products);
const sql = formatter.formatSelect(query);
expect(sql).toBe('SELECT id, name, category FROM dbo.Products');
});
});
5 changes: 2 additions & 3 deletions src/formatter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ export declare interface FormatterSettings {
export declare class SqlFormatter {
provider: any;
settings: FormatterSettings;
get validator(): ObjectNameValidator;


escape(value: any,unquoted?: boolean): string | any;
escapeConstant(value: any,unquoted?: boolean): string | any;


$or(...arg:any[]): string;
$and(...arg:any[]): string;
$not(arg:any): string;
Expand Down Expand Up @@ -82,6 +80,7 @@ export declare class SqlFormatter {
formatUpdate(query: QueryExpression | any): any;
formatDelete(query: QueryExpression | any): any;
escapeName(name: string): any;
escapeEntity(name: string): any;
formatFieldEx(obj: any, format: string): any;
format(obj: any, s?: string): any;

Expand Down
30 changes: 17 additions & 13 deletions src/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,15 +699,15 @@ class SqlFormatter {
let entityRef = obj.$ref[entity];
//escape entity ref
if (instanceOf(entityRef, QueryExpression)) {
escapedEntity = '(' + this.format(entityRef) + ') ' + $this.escapeName(entity);
escapedEntity = '(' + this.format(entityRef) + ') ' + $this.escapeEntity(entity);
}
else {
escapedEntity = entityRef.$as ? $this.escapeName(entityRef.name) + getAliasKeyword.bind($this)() + $this.escapeName(entityRef.$as) : $this.escapeName(entityRef.name);
escapedEntity = entityRef.$as ? $this.escapeEntity(entityRef.name) + getAliasKeyword.bind($this)() + $this.escapeName(entityRef.$as) : $this.escapeEntity(entityRef.name);
}
}
else {
//escape entity name
escapedEntity = $this.escapeName(entity);
escapedEntity = $this.escapeEntity(entity);
}
//add basic SELECT statement
if (Object.prototype.hasOwnProperty.call(obj, '$fixed') && obj.$fixed === true) {
Expand Down Expand Up @@ -737,7 +737,7 @@ class SqlFormatter {
else {
//get join table name
table = Object.key(x.$entity);
sql = sql.concat(' ' + joinType + ' JOIN ').concat($this.escapeName(table));
sql = sql.concat(' ' + joinType + ' JOIN ').concat($this.escapeEntity(table));
//add alias
if (x.$entity.$as)
sql = sql.concat(getAliasKeyword.bind($this)()).concat($this.escapeName(x.$entity.$as));
Expand Down Expand Up @@ -920,7 +920,7 @@ class SqlFormatter {
for (let prop in obj1)
if (Object.prototype.hasOwnProperty.call(obj1, prop))
props.push(prop);
sql = sql.concat('INSERT INTO ', self.escapeName(entity), '(', map(props, function (x) { return self.escapeName(x); }).join(', '), ') VALUES (',
sql = sql.concat('INSERT INTO ', self.escapeEntity(entity), '(', map(props, function (x) { return self.escapeName(x); }).join(', '), ') VALUES (',
map(props, function (x) {
let value = obj1[x];
return self.escape(value !== null ? value : null);
Expand All @@ -945,7 +945,7 @@ class SqlFormatter {
if (Object.prototype.hasOwnProperty.call(obj1, prop))
props.push(prop);
//add basic INSERT statement
sql = sql.concat('UPDATE ', self.escapeName(entity), ' SET ',
sql = sql.concat('UPDATE ', self.escapeEntity(entity), ' SET ',
map(props, function (x) {
let value = obj1[x];
return self.escapeName(x).concat('=', self.escape(value !== null ? value : null));
Expand All @@ -967,7 +967,7 @@ class SqlFormatter {
//get entity name
let entity = expr.$delete;
//add basic INSERT statement
sql = sql.concat('DELETE FROM ', this.escapeName(entity));
sql = sql.concat('DELETE FROM ', this.escapeEntity(entity));
if (expr.$where != null) {
sql = sql.concat(' WHERE ', this.formatWhere(expr.$where));
}
Expand All @@ -986,14 +986,18 @@ class SqlFormatter {
if (isNameReference(str)) {
str = trimNameReference(name);
}
return this.validator.escape(str, this.settings.nameFormat);
return ObjectNameValidator.validator.escape(str, this.settings.nameFormat);
}

/**
* @returns {ObjectNameValidator}
*/
get validator() {
return ObjectNameValidator.validator;
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);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/object-name.validator.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {SyncSeriesEventEmitter} from '@themost/events';

export declare class ObjectNameValidator {

static Patterns: {
Expand All @@ -12,6 +14,8 @@ export declare class ObjectNameValidator {
static readonly validator: ObjectNameValidator;

static use(validator: ObjectNameValidator): void;

validating: SyncSeriesEventEmitter<{ name: string, qualified?: boolean, valid?: boolean }>

constructor(pattern?: string);

Expand All @@ -21,10 +25,12 @@ export declare class ObjectNameValidator {

test(name: string, qualified?: boolean, throwError?: boolean): boolean;

exec(name: string, qualified?: boolean): boolean;

escape(name: string, format?: string): string;
}

export declare class InvalidObjectNameError extends Error {
code: string;
constructor(msg?: string);
}
}
37 changes: 34 additions & 3 deletions src/object-name.validator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/*eslint-env es6 */
// eslint-disable-next-line no-unused-vars
import {SyncSeriesEventEmitter} from '@themost/events';

class InvalidObjectNameError extends Error {
/**
Expand Down Expand Up @@ -29,6 +28,8 @@ class ObjectNameValidator {
* @type {RegExp}
*/
this.qualifiedPattern = new RegExp(`^\\*$|^${this.pattern.source}((\\.)${this.pattern.source})*(\\.\\*)?$`);

this.validating = new SyncSeriesEventEmitter();
}
/**
* @param {string} name - A string which defines a query field name or an alias
Expand All @@ -53,14 +54,44 @@ class ObjectNameValidator {
}
return valid;
}

/**
* @param {string} name
* @param {boolean=} qualified
* @returns {boolean}
*/
exec(name, qualified) {
let valid = undefined;
// validating event allow third party subscribers to validate object names
// and return a boolean value which indicates whether the object name is valid or not
const validatingEvent = {
name,
qualified,
valid
};
// raise validating event
this.validating.emit(validatingEvent);
// if valid property is boolean and has been set
if (typeof validatingEvent.valid === 'boolean') {
// throw error if name is invalid
if (validatingEvent.valid === false) {
throw new InvalidObjectNameError();
}
// otherwise, return the result
return validatingEvent.valid;
}
// this a fallback mechanism where the validating event has not been handled
return this.test(name, qualified);
}

/**
* Escapes the given base on the format provided
* @param {string} name - The object name
* @param {string=} format - The format that is going to be used for escaping name e.g. [$1] or `$1`
*/
escape(name, format) {
// validate qualified object name
this.test(name);
this.exec(name);
const pattern = new RegExp(this.pattern.source, 'g');
return name.replace(pattern, format || '$1');
}
Expand Down
4 changes: 2 additions & 2 deletions src/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -1638,7 +1638,7 @@ class QueryField {
if (fromEntity == null) {
throw new Error('Invalid argument. Expected a valid instance of query entity or string.');
}
ObjectNameValidator.validator.test(fromEntity, false);
ObjectNameValidator.validator.exec(fromEntity, false);
// get property
if (this.$name != null) {
if (typeof this.$name === 'string') {
Expand Down Expand Up @@ -1789,7 +1789,7 @@ class QueryField {
}
if (typeof alias !== 'string')
throw new Error('Invalid argument. Expected string');
ObjectNameValidator.validator.test(alias, false);
ObjectNameValidator.validator.exec(alias, false);
//get first property
let prop = Object.key(this);
if (isNil(prop))
Expand Down

0 comments on commit 10072bf

Please sign in to comment.