Skip to content

Commit

Permalink
Make sure sql function is called as a template (#1328)
Browse files Browse the repository at this point in the history
      Signed-off-by: Alexis Rico <[email protected]>
  • Loading branch information
SferaDev authored Jan 29, 2024
1 parent 062c262 commit 27773df
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/soft-ligers-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xata.io/client': patch
---

Fix untagged scenario for SQL function
55 changes: 47 additions & 8 deletions packages/client/src/sql/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
import { sqlQuery } from '../api';
import { XataPlugin, XataPluginOptions } from '../plugins';
import { isObject } from '../util/lang';
import { prepareParams } from './parameters';

export type SQLQueryParams<T = any[]> = {
/**
* The SQL statement to execute.
* @example
* ```ts
* const { records } = await xata.sql<TeamsRecord>({
* statement: `SELECT * FROM teams WHERE name = $1`,
* params: ['A name']
* });
* ```
*
* Be careful when using this with user input and use parametrized statements to avoid SQL injection.
*/
statement: string;
/**
* The parameters to pass to the SQL statement.
*/
params?: T;
/**
* The consistency level to use when executing the query.
*/
consistency?: 'strong' | 'eventual';
};

export type SQLQuery = TemplateStringsArray | SQLQueryParams | string;
export type SQLQuery = TemplateStringsArray | SQLQueryParams;

export type SQLPluginResult = <T>(
query: SQLQuery,
...parameters: any[]
) => Promise<{
export type SQLQueryResult<T> = {
/**
* The records returned by the query.
*/
records: T[];
/**
* The columns metadata returned by the query.
*/
columns?: Record<string, { type_name: string }>;
/**
* Optional warning message returned by the query.
*/
warning?: string;
}>;
};

export type SQLPluginResult = <T>(query: SQLQuery, ...parameters: any[]) => Promise<SQLQueryResult<T>>;

export class SQLPlugin extends XataPlugin {
build(pluginOptions: XataPluginOptions): SQLPluginResult {
return async <T>(param1: SQLQuery, ...param2: any[]) => {
const { statement, params, consistency } = prepareParams(param1, param2);
return async <T>(query: SQLQuery, ...parameters: any[]) => {
if (!isParamsObject(query) && (!isTemplateStringsArray(query) || !Array.isArray(parameters))) {
throw new Error('Invalid usage of `xata.sql`. Please use it as a tagged template or with an object.');
}

const { statement, params, consistency } = prepareParams(query, parameters);

const { records, warning, columns } = await sqlQuery({
pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', region: '{region}' },
Expand All @@ -34,3 +65,11 @@ export class SQLPlugin extends XataPlugin {
};
}
}

function isTemplateStringsArray(strings: unknown): strings is TemplateStringsArray {
return Array.isArray(strings) && 'raw' in strings && Array.isArray(strings.raw);
}

function isParamsObject(params: unknown): params is SQLQueryParams {
return isObject(params) && 'statement' in params;
}
2 changes: 1 addition & 1 deletion packages/client/src/sql/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function prepareValue(value: unknown) {
}
}

export function prepareParams(param1: SQLQuery, param2?: any[]) {
export function prepareParams(param1: SQLQuery | string, param2?: any[]) {
if (isString(param1)) {
return { statement: param1, params: param2?.map((value) => prepareValue(value)) };
}
Expand Down
24 changes: 23 additions & 1 deletion test/integration/sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('SQL proxy', () => {
test('read multiple teams ', async () => {
const teams = await xata.db.teams.create([{ name: '[A] Cars' }, { name: '[A] Planes' }]);

const { records, warning, columns } = await xata.sql<TeamsRecord>("SELECT * FROM teams WHERE name LIKE '[A] %'");
const { records, warning, columns } = await xata.sql<TeamsRecord>`SELECT * FROM teams WHERE name LIKE '[A] %'`;

expect(warning).toBeUndefined();
expect(records).toHaveLength(2);
Expand Down Expand Up @@ -220,4 +220,26 @@ describe('SQL proxy', () => {
expect(team).toBeDefined();
expect(team?.name).toBe('Team ships 2');
});

test("calling xata.sql as a function throws an error because it's not safe", async () => {
// @ts-expect-error - Testing invalid usage
await expect(xata.sql('SELECT * FROM teams')).rejects.toThrow(
'Invalid usage of `xata.sql`. Please use it as a tagged template or with an object.'
);
});

test('calling xata.sql with invalid prepared statement', async () => {
const order = 'ASC';
await expect(xata.sql<TeamsRecord>`SELECT * FROM teams ORDER BY name ${order}`).rejects.toThrow(
'invalid SQL: unused parameters: used 0 of 1 parameters'
);
});

test("calling xata.sql with invalid prepared statement doesn't throw an error when bypassing prepared statement protection", async () => {
const order = 'ASC';
const { records } = await xata.sql<TeamsRecord>({
statement: `SELECT * FROM teams ORDER BY name ${order}`
});
expect(records).toBeDefined();
});
});

0 comments on commit 27773df

Please sign in to comment.