Skip to content

Commit

Permalink
feat: convert token types to symbols
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed May 9, 2024
1 parent 8f05f8c commit 96db9f0
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 19 deletions.
11 changes: 11 additions & 0 deletions .changeset/funny-toes-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"slonik-sql-tag-raw": major
"@slonik/sql-tag": major
"slonik": major
---

Convert token types to symbols to ensures that SQL tokens cannot be injected from outside of the codebase, e.g. through JSON.

Thanks to @alxndrsn for reporting the issue.

Thanks to @danielrearden for suggesting a viable patch.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type PrimitiveValueExpression } from '../types';
import { FragmentToken } from '@slonik/sql-tag';
import {
createSqlTokenSqlFragment,
type FragmentSqlToken,
Expand Down Expand Up @@ -64,7 +65,7 @@ export const interpolatePositionalParameterReferences = (

return {
sql: resultSql,
type: 'SLONIK_TOKEN_FRAGMENT',
type: FragmentToken,
values: Object.freeze(resultValues),
};
};
15 changes: 15 additions & 0 deletions packages/sql-tag/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
export { createSqlTag } from './factories/createSqlTag';
export { createSqlTokenSqlFragment } from './factories/createSqlTokenSqlFragment';
export {
ArrayToken,
BinaryToken,
ComparisonPredicateToken,
DateToken,
FragmentToken,
IdentifierToken,
IntervalToken,
JsonBinaryToken,
JsonToken,
ListToken,
QueryToken,
TimestampToken,
UnnestToken,
} from './tokens';
export {
type ArraySqlToken,
type BinarySqlToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const createArraySqlFragment = (

if (
isSqlToken(token.memberType) &&
token.memberType.type === 'SLONIK_TOKEN_FRAGMENT'
Symbol.keyFor(token.memberType.type) === 'SLONIK_TOKEN_FRAGMENT'
) {
const sqlFragment = createSqlTokenSqlFragment(
token.memberType,
Expand Down
29 changes: 15 additions & 14 deletions packages/sql-tag/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
export const ArrayToken = 'SLONIK_TOKEN_ARRAY' as const;
export const BinaryToken = 'SLONIK_TOKEN_BINARY' as const;
export const ComparisonPredicateToken =
'SLONIK_TOKEN_COMPARISON_PREDICATE' as const;
export const DateToken = 'SLONIK_TOKEN_DATE' as const;
export const FragmentToken = 'SLONIK_TOKEN_FRAGMENT' as const;
export const IdentifierToken = 'SLONIK_TOKEN_IDENTIFIER' as const;
export const IntervalToken = 'SLONIK_TOKEN_INTERVAL' as const;
export const JsonBinaryToken = 'SLONIK_TOKEN_JSON_BINARY' as const;
export const JsonToken = 'SLONIK_TOKEN_JSON' as const;
export const ListToken = 'SLONIK_TOKEN_LIST' as const;
export const QueryToken = 'SLONIK_TOKEN_QUERY' as const;
export const TimestampToken = 'SLONIK_TOKEN_TIMESTAMP' as const;
export const UnnestToken = 'SLONIK_TOKEN_UNNEST' as const;
export const ArrayToken = Symbol.for('SLONIK_TOKEN_ARRAY');
export const BinaryToken = Symbol.for('SLONIK_TOKEN_BINARY');
export const ComparisonPredicateToken = Symbol.for(
'SLONIK_TOKEN_COMPARISON_PREDICATE',
);
export const DateToken = Symbol.for('SLONIK_TOKEN_DATE');
export const FragmentToken = Symbol.for('SLONIK_TOKEN_FRAGMENT');
export const IdentifierToken = Symbol.for('SLONIK_TOKEN_IDENTIFIER');
export const IntervalToken = Symbol.for('SLONIK_TOKEN_INTERVAL');
export const JsonBinaryToken = Symbol.for('SLONIK_TOKEN_JSON_BINARY');
export const JsonToken = Symbol.for('SLONIK_TOKEN_JSON');
export const ListToken = Symbol.for('SLONIK_TOKEN_LIST');
export const QueryToken = Symbol.for('SLONIK_TOKEN_QUERY');
export const TimestampToken = Symbol.for('SLONIK_TOKEN_TIMESTAMP');
export const UnnestToken = Symbol.for('SLONIK_TOKEN_UNNEST');
41 changes: 38 additions & 3 deletions packages/sql-tag/src/utilities/isSqlToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ const Tokens = [
UnnestToken,
] as const;

const tokenNamess = Tokens.map((token) => {
const tokenTypeName = Symbol.keyFor(token);

if (typeof tokenTypeName !== 'string') {
throw new UnexpectedStateError(
'Expected token type be a symbol with inferrable key',
);
}

return tokenTypeName;
});

export const isSqlToken = (subject: unknown): subject is SqlTokenType => {
if (typeof subject !== 'object' || subject === null) {
return false;
Expand All @@ -44,9 +56,32 @@ export const isSqlToken = (subject: unknown): subject is SqlTokenType => {
);
}

if (typeof subject.type !== 'string') {
throw new UnexpectedStateError('Expected type to be string.');
const tokenType = subject.type;

if (typeof tokenType !== 'symbol') {
throw new UnexpectedStateError('Expected type to be symbol.');
}

const tokenTypeName = Symbol.keyFor(tokenType);

if (typeof tokenTypeName !== 'string') {
throw new UnexpectedStateError(
'Expected token type to be a symbol with inferrable key',
);
}

return (Tokens as readonly string[]).includes(subject.type);
// It is worth clarifying that we don't care if symbols match.
// However, we do care that:
// 1) the type is a symbol; and
// 2) we can recognize the key
//
// The reason we care that the type is a symbol,
// is because it makes it impossible to inject
// it from outside of the codebase, e.g. through JSON.
//
// The reason we don't try to match instance of an object
// is because there is because it makes it difficult
// to version Slonik plugins that are used to
// construct custom SQL fragments.
return tokenNamess.includes(tokenTypeName);
};

0 comments on commit 96db9f0

Please sign in to comment.