Skip to content

Commit

Permalink
feat: add sql.uuid() (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus authored Dec 21, 2024
1 parent 96735e2 commit 755dc0b
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 1 deletion.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Note: Using this project does not require TypeScript. It is a regular ES6 module
* [`sql.timestamp`](#sqltimestamp)
* [`sql.unnest`](#sqlunnest)
* [`sql.unsafe`](#sqlunsafe)
* [`sql.uuid`](#sqluuid)
* [Query methods](#query-methods)
* [`any`](#any)
* [`anyFirst`](#anyfirst)
Expand Down Expand Up @@ -2212,6 +2213,33 @@ const result = await connection.one(sql.unsafe`
* Use `sql.typeAlias` to alias an existing type
* Use `sql.fragment` if you are writing a fragment of a query
### <code>sql.uuid</code>
```ts
(
uuid: string
) => TimestampSqlToken;
```
Inserts a UUID, e.g.
```ts
await connection.query(sql.unsafe`
SELECT ${sql.uuid('00000000-0000-0000-0000-000000000000')}
`);
```
Produces:
```ts
{
sql: 'SELECT $1::uuid',
values: [
'00000000-0000-0000-0000-000000000000'
]
}
```
## Query methods
### <code>any</code>
Expand Down
1 change: 1 addition & 0 deletions packages/slonik/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export {
type SqlTag,
type SqlToken,
type UnnestSqlToken,
type UuidSqlToken,
} from '@slonik/sql-tag';
export { stringifyDsn } from '@slonik/utilities';
export { parseDsn } from '@slonik/utilities';
25 changes: 25 additions & 0 deletions packages/sql-tag/src/factories/createSqlTag.test/uuid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FragmentToken } from '../../tokens';
import { createSqlTag } from '../createSqlTag';
import test from 'ava';

const sql = createSqlTag();

test('binds a uuid', (t) => {
const query = sql.fragment`SELECT ${sql.uuid(
'00000000-0000-0000-0000-000000000000',
)}`;

t.deepEqual(query, {
sql: 'SELECT $slonik_1::uuid',
type: FragmentToken,
values: ['00000000-0000-0000-0000-000000000000'],
});
});

test('throws if not valid uuid', (t) => {
const error = t.throws(() => {
sql.fragment`SELECT ${sql.uuid('1')}`;
});

t.is(error?.message, 'UUID parameter value must be a valid UUID.');
});
8 changes: 8 additions & 0 deletions packages/sql-tag/src/factories/createSqlTag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Logger } from '../Logger';
import { type UUID } from '../sqlFragmentFactories/createUuidSqlFragment';
import {
ArrayToken,
BinaryToken,
Expand All @@ -12,6 +13,7 @@ import {
QueryToken,
TimestampToken,
UnnestToken,
UuidToken,
} from '../tokens';
import {
type PrimitiveValueExpression,
Expand Down Expand Up @@ -229,5 +231,11 @@ export const createSqlTag = <
type: QueryToken,
});
},
uuid: (uuid) => {
return Object.freeze({
type: UuidToken,
uuid: uuid as UUID,
});
},
};
};
4 changes: 4 additions & 0 deletions packages/sql-tag/src/factories/createSqlTokenSqlFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createListSqlFragment } from '../sqlFragmentFactories/createListSqlFrag
import { createQuerySqlFragment } from '../sqlFragmentFactories/createQuerySqlFragment';
import { createTimestampSqlFragment } from '../sqlFragmentFactories/createTimestampSqlFragment';
import { createUnnestSqlFragment } from '../sqlFragmentFactories/createUnnestSqlFragment';
import { createUuidSqlFragment } from '../sqlFragmentFactories/createUuidSqlFragment';
import {
ArrayToken,
BinaryToken,
Expand All @@ -22,6 +23,7 @@ import {
QueryToken,
TimestampToken,
UnnestToken,
UuidToken,
} from '../tokens';
import { type SqlFragmentToken, type SqlToken as SqlTokenType } from '../types';
import { UnexpectedStateError } from '@slonik/errors';
Expand Down Expand Up @@ -54,6 +56,8 @@ export const createSqlTokenSqlFragment = (
return createTimestampSqlFragment(token, greatestParameterPosition);
} else if (token.type === UnnestToken) {
return createUnnestSqlFragment(token, greatestParameterPosition);
} else if (token.type === UuidToken) {
return createUuidSqlFragment(token, greatestParameterPosition);
}

throw new UnexpectedStateError('Unexpected token type.');
Expand Down
2 changes: 2 additions & 0 deletions packages/sql-tag/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
QueryToken,
TimestampToken,
UnnestToken,
UuidToken,
} from './tokens';
export {
type ArraySqlToken,
Expand All @@ -34,6 +35,7 @@ export {
type SqlToken,
type TimestampSqlToken,
type UnnestSqlToken,
type UuidSqlToken,
type ValueExpression,
} from './types';
export { isSqlToken } from './utilities/isSqlToken';
28 changes: 28 additions & 0 deletions packages/sql-tag/src/sqlFragmentFactories/createUuidSqlFragment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FragmentToken } from '../tokens';
import { type SqlFragmentToken, type UuidSqlToken } from '../types';
import { formatSlonikPlaceholder } from '../utilities/formatSlonikPlaceholder';
import { InvalidInputError } from '@slonik/errors';

export type UUID = `${string}-${string}-${string}-${string}-${string}`;

// eslint-disable-next-line func-style
function isValidUuid(uuid: string): uuid is UUID {
const uuidRegex = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu;

return uuidRegex.test(uuid);
}

export const createUuidSqlFragment = (
token: UuidSqlToken,
greatestParameterPosition: number,
): SqlFragmentToken => {
if (!isValidUuid(token.uuid)) {
throw new InvalidInputError('UUID parameter value must be a valid UUID.');
}

return {
sql: formatSlonikPlaceholder(greatestParameterPosition + 1) + '::uuid',
type: FragmentToken,
values: [token.uuid],
};
};
1 change: 1 addition & 0 deletions packages/sql-tag/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ 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');
export const UuidToken = Symbol.for('SLONIK_TOKEN_UUID');
9 changes: 8 additions & 1 deletion packages/sql-tag/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export type SqlTag<
template: TemplateStringsArray,
...values: ValueExpression[]
) => QuerySqlToken;
uuid: (uuid: string) => UuidSqlToken;
};

export type SqlToken =
Expand All @@ -150,7 +151,8 @@ export type SqlToken =
| ListSqlToken
| QuerySqlToken
| TimestampSqlToken
| UnnestSqlToken;
| UnnestSqlToken
| UuidSqlToken;

export type TimestampSqlToken = {
readonly date: Date;
Expand All @@ -165,6 +167,11 @@ export type UnnestSqlToken = {
readonly type: typeof tokens.UnnestToken;
};

export type UuidSqlToken = {
readonly type: typeof tokens.UuidToken;
readonly uuid: `${string}-${string}-${string}-${string}-${string}`;
};

export type ValueExpression =
| PrimitiveValueExpression
| SqlFragmentToken
Expand Down
2 changes: 2 additions & 0 deletions packages/sql-tag/src/utilities/isSqlToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
QueryToken,
TimestampToken,
UnnestToken,
UuidToken,
} from '../tokens';
import { type SqlToken as SqlTokenType } from '../types';
import { hasOwnProperty } from './hasOwnProperty';
Expand All @@ -31,6 +32,7 @@ const Tokens = [
QueryToken,
TimestampToken,
UnnestToken,
UuidToken,
] as const;

const tokenNamess = Tokens.map((token) => {
Expand Down

0 comments on commit 755dc0b

Please sign in to comment.