Skip to content

Commit 0bbedc7

Browse files
committed
feat(core,build): add sanitizer and parameterizer
- passing context to filter runner - add santizier filter builder and runner - add TemplateInput class to control parameterize logics - update req runner to inject parameterizers
1 parent 390df54 commit 0bbedc7

File tree

20 files changed

+266
-53
lines changed

20 files changed

+266
-53
lines changed

labs/playground1/sqls/artist/artist.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ request:
55
description: constituent id
66
validators:
77
- required
8+
exampleParameter:
9+
id: "1"

labs/playground1/sqls/artist/works.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ request:
55
description: constituent id
66
validators:
77
- required
8+
exampleParameter:
9+
id: '1'

packages/build/src/lib/schema-parser/middleware/responseSampler.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { inject } from 'inversify';
22
import { RawAPISchema, SchemaParserMiddleware } from './middleware';
33
import {
44
APISchema,
5+
DataSource,
56
FieldDataType,
67
ResponseProperty,
78
TemplateEngine,
@@ -11,12 +12,15 @@ import { unionBy } from 'lodash';
1112

1213
export class ResponseSampler extends SchemaParserMiddleware {
1314
private templateEngine: TemplateEngine;
15+
private dataSource: DataSource;
1416

1517
constructor(
16-
@inject(CORE_TYPES.TemplateEngine) templateEngine: TemplateEngine
18+
@inject(CORE_TYPES.TemplateEngine) templateEngine: TemplateEngine,
19+
@inject(CORE_TYPES.DataSource) dataSource: DataSource
1720
) {
1821
super();
1922
this.templateEngine = templateEngine;
23+
this.dataSource = dataSource;
2024
}
2125

2226
public async handle(
@@ -27,17 +31,19 @@ export class ResponseSampler extends SchemaParserMiddleware {
2731
const schema = rawSchema as APISchema;
2832
if (!schema.exampleParameter) return;
2933

34+
const prepared = await this.dataSource.prepare(schema.exampleParameter);
35+
3036
const response = await this.templateEngine.execute(
3137
schema.templateSource,
32-
{ context: { params: schema.exampleParameter } },
38+
{ ['_prepared']: prepared },
3339
// We only need the columns of this query, so we set offset/limit both to 0 here.
3440
{
3541
limit: 0,
3642
offset: 0,
3743
}
3844
);
3945
// TODO: I haven't known the response of queryBuilder.value(), assume that there is a "columns" property that indicates the columns' name and type here.
40-
const columns: { name: string; type: string }[] = response.columns;
46+
const columns: { name: string; type: string }[] = response.getColumns();
4147
const responseColumns = this.normalizeResponseColumns(columns);
4248
schema.response = this.mergeResponse(
4349
schema.response || [],

packages/core/src/lib/data-query/executor.ts

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface IExecutor {
88
query: string,
99
bindParams: BindParameters
1010
): Promise<IDataQueryBuilder>;
11+
12+
getDataSource(): Promise<DataSource>;
1113
}
1214

1315
@injectable()
@@ -16,6 +18,11 @@ export class QueryExecutor implements IExecutor {
1618
constructor(@inject(TYPES.DataSource) dataSource: DataSource) {
1719
this.dataSource = dataSource;
1820
}
21+
22+
public async getDataSource() {
23+
return this.dataSource;
24+
}
25+
1926
/**
2027
* create data query builder
2128
* @returns

packages/core/src/lib/data-source/pg.ts

+4-15
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
DataSource,
66
ExecuteOptions,
77
IdentifierParameters,
8-
RequestParameters,
8+
RequestParameter,
99
VulcanExtensionId,
1010
VulcanInternalExtension,
1111
} from '../../models/extensions';
@@ -24,19 +24,8 @@ export class PGDataSource extends DataSource {
2424
},
2525
};
2626
}
27-
public async prepare(params: RequestParameters) {
28-
const identifiers = {} as IdentifierParameters;
29-
const binds = {} as BindParameters;
30-
let index = 1;
31-
for (const key of Object.keys(params)) {
32-
const identifier = `$${index}`;
33-
identifiers[key] = identifier;
34-
binds[identifier] = params[key];
35-
index += 1;
36-
}
37-
return {
38-
identifiers,
39-
binds,
40-
};
27+
28+
public async prepare({ parameterIndex }: RequestParameter) {
29+
return `$${parameterIndex}`;
4130
}
4231
}

packages/core/src/lib/template-engine/built-in-extensions/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ import SqlHelper from './sql-helper';
44
import Validator from './validator';
55

66
export default [CustomError, QueryBuilder, SqlHelper, Validator];
7+
8+
export * from './query-builder';

packages/core/src/lib/template-engine/built-in-extensions/query-builder/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export const METADATA_NAME = 'builder.vulcan.com';
33
export const EXECUTE_COMMAND_NAME = 'value';
44
export const EXECUTE_FILTER_NAME = 'execute';
55
export const REFERENCE_SEARCH_MAX_DEPTH = 100;
6+
export const SANITIZE_SOURCES = ['context'];
7+
export const SANITIZER_NAME = 'sanitize';
8+
export const PARAMETERIZER_VAR_NAME = 'parameterizer';

packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorRunner.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { IDataQueryBuilder } from '@vulcan-sql/core/data-query';
2-
import { FilterRunner, VulcanInternalExtension } from '@vulcan-sql/core/models';
2+
import {
3+
FilterRunner,
4+
FilterRunnerTransformOptions,
5+
VulcanInternalExtension,
6+
} from '@vulcan-sql/core/models';
37
import { EXECUTE_FILTER_NAME } from './constants';
48

59
@VulcanInternalExtension()
610
export class ExecutorRunner extends FilterRunner {
711
public filterName = EXECUTE_FILTER_NAME;
812

9-
public async transform({ value }: { value: any; args: any[] }): Promise<any> {
13+
public async transform({
14+
value,
15+
}: FilterRunnerTransformOptions): Promise<any> {
1016
const builder: IDataQueryBuilder = value;
1117
return builder.value();
1218
}

packages/core/src/lib/template-engine/built-in-extensions/query-builder/index.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,16 @@ import { ReqTagBuilder } from './reqTagBuilder';
22
import { ReqTagRunner } from './reqTagRunner';
33
import { ExecutorRunner } from './executorRunner';
44
import { ExecutorBuilder } from './executorBuilder';
5+
import { SanitizerBuilder } from './sanitizerBuilder';
6+
import { SanitizerRunner } from './sanitizerRunner';
57

6-
export default [ReqTagBuilder, ReqTagRunner, ExecutorRunner, ExecutorBuilder];
8+
export default [
9+
ReqTagBuilder,
10+
ReqTagRunner,
11+
ExecutorRunner,
12+
ExecutorBuilder,
13+
SanitizerBuilder,
14+
SanitizerRunner,
15+
];
16+
17+
export * from './templateInput';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { DataSource } from '@vulcan-sql/core/models';
2+
3+
export class Parameterizer {
4+
private parameterIndex = 1;
5+
private sealed = false;
6+
// We MUST not use pure object here because we care about the order of the keys.
7+
// https://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order
8+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description
9+
private idToValueMapping = new Map<string, any>();
10+
private dataSource: DataSource;
11+
12+
constructor(dataSource: DataSource) {
13+
this.dataSource = dataSource;
14+
}
15+
16+
public async generateIdentifier(value: any): Promise<string> {
17+
if (this.sealed)
18+
throw new Error(
19+
`This parameterizer has been sealed, we might use the parameterizer from a wrong request scope.`
20+
);
21+
const id = await this.dataSource.prepare({
22+
parameterIndex: this.parameterIndex++,
23+
value,
24+
});
25+
this.idToValueMapping.set(id, value);
26+
return id;
27+
}
28+
29+
public seal() {
30+
this.sealed = true;
31+
}
32+
33+
public getBinding() {
34+
return this.idToValueMapping;
35+
}
36+
}

packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
TagRunnerOptions,
77
VulcanInternalExtension,
88
} from '@vulcan-sql/core/models';
9-
import { FINIAL_BUILDER_NAME } from './constants';
9+
import { FINIAL_BUILDER_NAME, PARAMETERIZER_VAR_NAME } from './constants';
10+
import { Parameterizer } from './parameterizer';
1011

1112
@VulcanInternalExtension()
1213
export class ReqTagRunner extends TagRunner {
@@ -24,16 +25,26 @@ export class ReqTagRunner extends TagRunner {
2425

2526
public async run({ context, args, contentArgs }: TagRunnerOptions) {
2627
const name = args[0];
28+
29+
const dataSource = await this.executor.getDataSource();
30+
31+
const parameterizer = new Parameterizer(dataSource);
32+
// parameterizer from parent, we should set it back after rendered our context.
33+
const parentParameterizer = context.lookup(PARAMETERIZER_VAR_NAME);
34+
context.setVariable(PARAMETERIZER_VAR_NAME, parameterizer);
2735
let query = '';
2836
for (let index = 0; index < contentArgs.length; index++) {
2937
query += await contentArgs[index]();
3038
}
39+
// Seal current parameterizer to avoid incorrect usage.
40+
parameterizer.seal();
41+
context.setVariable(PARAMETERIZER_VAR_NAME, parentParameterizer);
3142
query = query
3243
.split(/\r?\n/)
3344
.filter((line) => line.trim().length > 0)
3445
.join('\n');
3546
// Get bind real parameters and pass to data query builder for data source used.
36-
const binds = (context.ctx || {})['_paramBinds'] || {};
47+
const binds = parameterizer.getBinding();
3748
const builder = await this.executor.createBuilder(query, binds);
3849
context.setVariable(name, builder);
3950

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
FilterBuilder,
3+
VulcanInternalExtension,
4+
} from '@vulcan-sql/core/models';
5+
import * as nunjucks from 'nunjucks';
6+
import { visitChildren } from '../../extension-utils';
7+
import {
8+
REFERENCE_SEARCH_MAX_DEPTH,
9+
SANITIZER_NAME,
10+
SANITIZE_SOURCES,
11+
} from './constants';
12+
13+
@VulcanInternalExtension()
14+
export class SanitizerBuilder extends FilterBuilder {
15+
public filterName = SANITIZER_NAME;
16+
public override onVisit(node: nunjucks.nodes.Node): void {
17+
if (node instanceof nunjucks.nodes.Root) this.addSanitizer(node);
18+
}
19+
20+
private addSanitizer(node: nunjucks.nodes.Node, parentHasOutputNode = false) {
21+
visitChildren(node, (child, replace) => {
22+
if (child instanceof nunjucks.nodes.LookupVal) {
23+
const source = this.findSourceOfLookUpNode(child);
24+
if (SANITIZE_SOURCES.includes(source.value)) {
25+
const filter = new nunjucks.nodes.Filter(node.lineno, node.colno);
26+
filter.name = new nunjucks.nodes.Symbol(
27+
node.lineno,
28+
node.colno,
29+
SANITIZER_NAME
30+
);
31+
const args = new nunjucks.nodes.NodeList(node.lineno, node.colno);
32+
// The first argument is the target of the filter
33+
args.addChild(child);
34+
// The second argument indicates whether it should be parameterized, we only parameterize parameters when once of parent nodes is a Output node.
35+
const shouldBeParameterized = new nunjucks.nodes.Literal(
36+
node.lineno,
37+
node.colno,
38+
`${parentHasOutputNode || node instanceof nunjucks.nodes.Output}`
39+
);
40+
args.addChild(shouldBeParameterized);
41+
filter.args = args;
42+
replace(filter);
43+
}
44+
} else {
45+
this.addSanitizer(
46+
child,
47+
parentHasOutputNode || node instanceof nunjucks.nodes.Output
48+
);
49+
}
50+
});
51+
}
52+
53+
private findSourceOfLookUpNode(
54+
node: nunjucks.nodes.LookupVal
55+
): nunjucks.nodes.Symbol {
56+
let depth = 0;
57+
let source: typeof node.target = node.target;
58+
while (!(source instanceof nunjucks.nodes.Symbol)) {
59+
depth++;
60+
if (depth > REFERENCE_SEARCH_MAX_DEPTH) {
61+
throw new Error('Max depth reached');
62+
}
63+
64+
if (source instanceof nunjucks.nodes.LookupVal) {
65+
// LookupVal: parent.source
66+
source = source.target;
67+
} else {
68+
// FunCall: parent().source
69+
source = source.name;
70+
}
71+
72+
if (!source) {
73+
throw new Error(`Can find the source of node ${node}`);
74+
}
75+
}
76+
return source;
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
FilterRunner,
3+
FilterRunnerTransformOptions,
4+
VulcanInternalExtension,
5+
} from '@vulcan-sql/core/models';
6+
import { PARAMETERIZER_VAR_NAME, SANITIZER_NAME } from './constants';
7+
import { TemplateInput } from './templateInput';
8+
9+
@VulcanInternalExtension()
10+
export class SanitizerRunner extends FilterRunner {
11+
public filterName = SANITIZER_NAME;
12+
13+
public async transform({
14+
value,
15+
args,
16+
context,
17+
}: FilterRunnerTransformOptions): Promise<any> {
18+
let input: TemplateInput;
19+
// Wrap the value to template input to parameterized
20+
if (value instanceof TemplateInput) input = value;
21+
else {
22+
input = new TemplateInput(value);
23+
}
24+
25+
const parameterizer = context.lookup(PARAMETERIZER_VAR_NAME);
26+
if (!parameterizer) throw new Error(`No parameterizer found`);
27+
return args[0] === 'true'
28+
? await input.parameterize(parameterizer)
29+
: input.raw();
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Parameterizer } from './parameterizer';
2+
3+
export class TemplateInput {
4+
private rawValue: any;
5+
6+
constructor(rawValue: any) {
7+
this.rawValue = rawValue;
8+
}
9+
10+
public raw() {
11+
return this.rawValue;
12+
}
13+
14+
public parameterize(parameterizer: Parameterizer) {
15+
return parameterizer.generateIdentifier(this.rawValue);
16+
}
17+
}

packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterRunner.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { FilterRunner, VulcanInternalExtension } from '@vulcan-sql/core/models';
1+
import {
2+
FilterRunner,
3+
FilterRunnerTransformOptions,
4+
VulcanInternalExtension,
5+
} from '@vulcan-sql/core/models';
26
import { uniq, uniqBy } from 'lodash';
37

48
@VulcanInternalExtension()
@@ -7,10 +11,7 @@ export class UniqueFilterRunner extends FilterRunner {
711
public async transform({
812
value,
913
args,
10-
}: {
11-
value: any[];
12-
args: any[];
13-
}): Promise<any> {
14+
}: FilterRunnerTransformOptions): Promise<any> {
1415
if (args.length === 0) {
1516
return uniq(value);
1617
}

0 commit comments

Comments
 (0)