-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added tagged template literal for SQL
- Loading branch information
1 parent
9b8421a
commit 3914509
Showing
5 changed files
with
248 additions
and
32 deletions.
There are no files selected for viewing
134 changes: 134 additions & 0 deletions
134
src/packages/dumbo/src/core/sql/index.tagged-template.unit.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import assert from 'assert'; | ||
import { describe, it } from 'node:test'; | ||
import { SQL, identifier, isSQL, literal, plainString, rawSql } from '.'; | ||
|
||
void describe('SQL Tagged Template Literal', () => { | ||
void it('should format literals correctly', () => { | ||
const name: string = 'John Doe'; | ||
const query: string = SQL`SELECT * FROM users WHERE name = ${literal(name)};`; | ||
|
||
// Expected output directly without using pg-format | ||
assert.strictEqual(query, "SELECT * FROM users WHERE name = 'John Doe';"); | ||
}); | ||
|
||
void it('should format identifiers correctly', () => { | ||
const tableName: string = 'users'; | ||
const columnName: string = 'name'; | ||
const query: string = SQL`SELECT ${identifier(columnName)} FROM ${identifier(tableName)};`; | ||
|
||
// Expected output directly without using pg-format | ||
assert.strictEqual(query, 'SELECT name FROM users;'); | ||
}); | ||
|
||
void it('should format identifiers with CAPS correctly', () => { | ||
const tableName: string = 'Users'; | ||
const columnName: string = 'Name'; | ||
const query: string = SQL`SELECT ${identifier(columnName)} FROM ${identifier(tableName)};`; | ||
|
||
// Expected output directly without using pg-format | ||
assert.strictEqual(query, 'SELECT "Name" FROM "Users";'); | ||
}); | ||
|
||
void it('should NOT format plain strings without escaping', () => { | ||
const unsafeString: string = "some'unsafe"; | ||
const query: string = SQL`SELECT ${plainString(unsafeString)};`; | ||
|
||
// Plain string without escaping | ||
assert.strictEqual(query, "SELECT some'unsafe;"); | ||
}); | ||
|
||
void it('should handle default literal formatting for plain values', () => { | ||
const name: string = 'John Doe'; | ||
const age: number = 30; | ||
const query: string = SQL`INSERT INTO users (name, age) VALUES (${name}, ${age});`; | ||
|
||
// Default literal formatting for plain values | ||
assert.strictEqual( | ||
query, | ||
"INSERT INTO users (name, age) VALUES ('John Doe', 30);", | ||
); | ||
}); | ||
|
||
void it('should handle mixed types of formatting', () => { | ||
const name: string = 'John Doe'; | ||
const age: number = 30; | ||
const table: string = 'users'; | ||
const query: string = SQL` | ||
INSERT INTO ${identifier(table)} (name, age) | ||
VALUES (${literal(name)}, ${age}) | ||
RETURNING name, age; | ||
`; | ||
|
||
// Mixed formatting for identifiers and literals | ||
assert.strictEqual( | ||
query, | ||
` | ||
INSERT INTO users (name, age) | ||
VALUES ('John Doe', 30) | ||
RETURNING name, age; | ||
`, | ||
); | ||
}); | ||
|
||
void it('should work with raw SQL', () => { | ||
const rawQuery: string = rawSql('SELECT * FROM users'); | ||
assert.strictEqual(rawQuery, 'SELECT * FROM users'); | ||
assert.strictEqual(isSQL(rawQuery), true); | ||
}); | ||
|
||
void it('should NOT recognize valid SQL using isSQL', () => { | ||
const validSql: string = SQL`SELECT * FROM users;`; | ||
const invalidSql: string = 'SELECT * FROM users;'; | ||
|
||
assert.strictEqual(isSQL(validSql), true); | ||
assert.strictEqual(isSQL(invalidSql), true); | ||
}); | ||
|
||
void it('should escape special characters in literals', () => { | ||
const unsafeValue: string = "O'Reilly"; | ||
const query: string = SQL`SELECT * FROM users WHERE name = ${literal(unsafeValue)};`; | ||
|
||
// SQL-safe escaping of single quote characters | ||
assert.strictEqual(query, "SELECT * FROM users WHERE name = 'O''Reilly';"); | ||
}); | ||
|
||
void it('should correctly format empty strings and falsy values', () => { | ||
const emptyString: string = ''; | ||
const nullValue: null = null; | ||
const zeroValue: number = 0; | ||
|
||
const query: string = SQL`INSERT INTO test (col1, col2, col3) | ||
VALUES (${literal(emptyString)}, ${literal(nullValue)}, ${literal(zeroValue)});`; | ||
|
||
// Handle empty string, null, and zero correctly | ||
assert.strictEqual( | ||
query, | ||
`INSERT INTO test (col1, col2, col3) | ||
VALUES ('', NULL, '0');`, | ||
); | ||
}); | ||
|
||
void it('should handle arrays of values using literals', () => { | ||
const values: string[] = ['John', 'Doe', '30']; | ||
const query: string = SQL`INSERT INTO users (first_name, last_name, age) | ||
VALUES (${literal(values[0])}, ${literal(values[1])}, ${literal(values[2])});`; | ||
|
||
// Handle array elements using literal formatting | ||
assert.strictEqual( | ||
query, | ||
`INSERT INTO users (first_name, last_name, age) | ||
VALUES ('John', 'Doe', '30');`, | ||
); | ||
}); | ||
|
||
void it('should handle SQL injections attempts safely', () => { | ||
const unsafeInput: string = "'; DROP TABLE users; --"; | ||
const query: string = SQL`SELECT * FROM users WHERE name = ${literal(unsafeInput)};`; | ||
|
||
// Escape SQL injection attempts correctly | ||
assert.strictEqual( | ||
query, | ||
"SELECT * FROM users WHERE name = '''; DROP TABLE users; --';", | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,79 @@ | ||
import format from './pg-format'; | ||
// TODO: add core formatter, when adding other database type | ||
|
||
export type SQL = string & { __brand: 'sql' }; | ||
type SQL = string & { __brand: 'sql' }; | ||
|
||
export const sql = (sqlQuery: string, ...params: unknown[]): SQL => { | ||
const sql = (sqlQuery: string, ...params: unknown[]): SQL => { | ||
return format(sqlQuery, ...params) as SQL; | ||
}; | ||
|
||
export const rawSql = (sqlQuery: string): SQL => { | ||
const rawSql = (sqlQuery: string): SQL => { | ||
return sqlQuery as SQL; | ||
}; | ||
|
||
const isSQL = (literal: unknown): literal is SQL => | ||
literal !== undefined && literal !== null && typeof literal === 'string'; | ||
|
||
type SQLValue = | ||
| { type: 'literal'; value: unknown } // Literal types (e.g., strings, numbers) | ||
| { type: 'identifier'; value: string } // Identifier types (e.g., table/column names) | ||
| { type: 'plainString'; value: string }; // Plain string types (unsafe, unescaped) | ||
|
||
// Wrapping functions for explicit formatting | ||
const literal = (value: unknown) => ({ type: 'literal', value }); | ||
const identifier = (value: string) => ({ type: 'identifier', value }); | ||
const plainString = (value: string) => ({ type: 'plainString', value }); | ||
|
||
const defaultFormat = (value: unknown) => { | ||
if (typeof value === 'string') { | ||
return format('%L', value); | ||
} else if (typeof value === 'number') { | ||
return value.toString(); | ||
} else if (typeof value === 'bigint') { | ||
return format('%L', value); | ||
} else if (value instanceof Date) { | ||
return format('%L', value); | ||
} else if (Array.isArray(value)) { | ||
return format('(%L)', value); | ||
} else { | ||
return format('%L', value); | ||
} | ||
}; | ||
|
||
function SQL(strings: TemplateStringsArray, ...values: unknown[]): SQL { | ||
return strings | ||
.map((string, index) => { | ||
let formattedValue = ''; | ||
|
||
if (index < values.length) { | ||
const value = values[index]; | ||
|
||
if ( | ||
value && | ||
typeof value === 'object' && | ||
'type' in value && | ||
'value' in value | ||
) { | ||
const sqlValue = value as SQLValue; | ||
switch (sqlValue.type) { | ||
case 'literal': | ||
formattedValue = format('%L', sqlValue.value); | ||
break; | ||
case 'identifier': | ||
formattedValue = format('%I', sqlValue.value); | ||
break; | ||
case 'plainString': | ||
formattedValue = sqlValue.value; | ||
break; | ||
} | ||
} else { | ||
formattedValue = defaultFormat(value); | ||
} | ||
} | ||
|
||
return string + formattedValue; | ||
}) | ||
.join('') as SQL; | ||
} | ||
|
||
export { SQL, identifier, isSQL, literal, plainString, rawSql, sql }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters