Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: gajus/slonik
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v31.1.0
Choose a base ref
...
head repository: gajus/slonik
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v31.2.0
Choose a head ref
  • 2 commits
  • 9 files changed
  • 1 contributor

Commits on Sep 10, 2022

  1. feat: add typeAlias (fixes #408)

    gajus committed Sep 10, 2022
    Copy the full SHA
    91128d1 View commit details
  2. fix: ignore ts error

    gajus committed Sep 10, 2022
    Copy the full SHA
    0303979 View commit details
Showing with 262 additions and 150 deletions.
  1. +20 −0 .README/SQL_TAG.md
  2. +23 −0 README.md
  3. +143 −129 src/factories/createSqlTag.ts
  4. +2 −1 src/types.ts
  5. +1 −1 test/dts.ts
  6. +0 −18 test/slonik/templateTags/sql/sql.ts
  7. +21 −0 test/slonik/templateTags/sql/type.ts
  8. +42 −0 test/slonik/templateTags/sql/typeAlias.ts
  9. +10 −1 test/types.ts
20 changes: 20 additions & 0 deletions .README/SQL_TAG.md
Original file line number Diff line number Diff line change
@@ -23,6 +23,26 @@ import {
const sql = createSqlTag();
```

### Type aliases

You can create a `sql` tag with a predefined set of Zod type aliases that can be later referenced when creating a query with [runtime validation](#runtime-validation), e.g.

```ts
const sql = createSqlTag({
typeAliases: {
id: z.object({
id: z.number(),
}),
}
})

const personId = await pool.oneFirst(
sql.typeAlias('id')`
SELECT id
FROM person
`);
```

### Typing `sql` tag

`sql` has a generic interface, meaning that you can supply it with the type that represents the expected result of the query, e.g.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -101,6 +101,7 @@ Note: Using this project does not require TypeScript. It is a regular ES6 module
* [Inferring types](#user-content-slonik-runtime-validation-inferring-types)
* [Transforming results](#user-content-slonik-runtime-validation-transforming-results)
* [`sql` tag](#user-content-slonik-sql-tag)
* [Type aliases](#user-content-slonik-sql-tag-type-aliases)
* [Typing `sql` tag](#user-content-slonik-sql-tag-typing-sql-tag)
* [Value placeholders](#user-content-slonik-value-placeholders)
* [Tagged template literals](#user-content-slonik-value-placeholders-tagged-template-literals)
@@ -1392,6 +1393,28 @@ import {
const sql = createSqlTag();
```

<a name="user-content-slonik-sql-tag-type-aliases"></a>
<a name="slonik-sql-tag-type-aliases"></a>
### Type aliases

You can create a `sql` tag with a predefined set of Zod type aliases that can be later referenced when creating a query with [runtime validation](#user-content-runtime-validation), e.g.

```ts
const sql = createSqlTag({
typeAliases: {
id: z.object({
id: z.number(),
}),
}
})

const personId = await pool.oneFirst(
sql.typeAlias('id')`
SELECT id
FROM person
`);
```

<a name="user-content-slonik-sql-tag-typing-sql-tag"></a>
<a name="slonik-sql-tag-typing-sql-tag"></a>
### Typing <code>sql</code> tag
272 changes: 143 additions & 129 deletions src/factories/createSqlTag.ts
Original file line number Diff line number Diff line change
@@ -109,164 +109,178 @@ const createQuery = (
};
};

const sql = (
parts: readonly string[],
...args: readonly ValueExpression[]
) => {
const {
sql: sqlText,
values,
} = createQuery(parts, args);

const query = {
sql: sqlText,
type: SqlToken,
values,
};
type SqlTagConfiguration<B> = {
typeAliases?: Record<string, B>,
};

Object.defineProperty(query, 'sql', {
configurable: false,
enumerable: true,
writable: false,
});
export const createSqlTag = <B extends ZodTypeAny, T extends QueryResultRow = QueryResultRow>(configuration: SqlTagConfiguration<B> = {}) => {
const typeAliases = configuration.typeAliases ?? {};

return query as unknown as SqlSqlToken<QueryResultRow>;
};
const sql = (
parts: readonly string[],
...args: readonly ValueExpression[]
) => {
const {
sql: sqlText,
values,
} = createQuery(parts, args);

sql.array = (
values: readonly PrimitiveValueExpression[],
memberType: SqlTokenType | TypeNameIdentifier,
): ArraySqlToken => {
return {
memberType,
type: ArrayToken,
values,
const query = {
sql: sqlText,
type: SqlToken,
values,
};

Object.defineProperty(query, 'sql', {
configurable: false,
enumerable: true,
writable: false,
});

return query as unknown as SqlSqlToken<QueryResultRow>;
};
};

sql.binary = (
data: Buffer,
): BinarySqlToken => {
return {
data,
type: BinaryToken,
sql.array = (
values: readonly PrimitiveValueExpression[],
memberType: SqlTokenType | TypeNameIdentifier,
): ArraySqlToken => {
return {
memberType,
type: ArrayToken,
values,
};
};
};

sql.date = (
date: Date,
): DateSqlToken => {
return {
date,
type: DateToken,
sql.binary = (
data: Buffer,
): BinarySqlToken => {
return {
data,
type: BinaryToken,
};
};
};

sql.identifier = (
names: readonly string[],
): IdentifierSqlToken => {
return {
names,
type: IdentifierToken,
sql.date = (
date: Date,
): DateSqlToken => {
return {
date,
type: DateToken,
};
};
};

sql.interval = (
interval: IntervalInput,
): IntervalSqlToken => {
return {
interval,
type: IntervalToken,
sql.identifier = (
names: readonly string[],
): IdentifierSqlToken => {
return {
names,
type: IdentifierToken,
};
};
};

sql.join = (
members: readonly ValueExpression[],
glue: SqlSqlToken,
): ListSqlToken => {
return {
glue,
members,
type: ListToken,
sql.interval = (
interval: IntervalInput,
): IntervalSqlToken => {
return {
interval,
type: IntervalToken,
};
};
};

sql.json = (
value: SerializableValue,
): JsonSqlToken => {
return {
type: JsonToken,
value,
sql.join = (
members: readonly ValueExpression[],
glue: SqlSqlToken,
): ListSqlToken => {
return {
glue,
members,
type: ListToken,
};
};
};

sql.jsonb = (
value: SerializableValue,
): JsonBinarySqlToken => {
return {
type: JsonBinaryToken,
value,
sql.json = (
value: SerializableValue,
): JsonSqlToken => {
return {
type: JsonToken,
value,
};
};
};

sql.literalValue = (
value: string,
): SqlSqlToken => {
return {
parser: z.any({}),
sql: escapeLiteralValue(value),
type: SqlToken,
values: [],
sql.jsonb = (
value: SerializableValue,
): JsonBinarySqlToken => {
return {
type: JsonBinaryToken,
value,
};
};
};

sql.timestamp = (
date: Date,
): TimestampSqlToken => {
return {
date,
type: TimestampToken,
sql.literalValue = (
value: string,
): SqlSqlToken => {
return {
parser: z.any({}),
sql: escapeLiteralValue(value),
type: SqlToken,
values: [],
};
};
};

sql.type = <T extends ZodTypeAny>(parser: T) => {
return (
parts: readonly string[],
...args: readonly ValueExpression[]
) => {
const {
sql: sqlText,
values,
} = createQuery(parts, args);
sql.timestamp = (
date: Date,
): TimestampSqlToken => {
return {
date,
type: TimestampToken,
};
};

const query = {
parser,
sql: sqlText,
type: SqlToken,
values,
sql.type = <Y extends ZodTypeAny>(parser: Y) => {
return (
parts: readonly string[],
...args: readonly ValueExpression[]
) => {
const {
sql: sqlText,
values,
} = createQuery(parts, args);
const query = {
parser,
sql: sqlText,
type: SqlToken,
values,
};

Object.defineProperty(query, 'sql', {
configurable: false,
enumerable: true,
writable: false,
});

return query;
};
};

Object.defineProperty(query, 'sql', {
configurable: false,
enumerable: true,
writable: false,
});
sql.typeAlias = <Y extends keyof typeof typeAliases>(parserAlias: Y) => {
if (!typeAliases[parserAlias]) {
throw new Error('Type alias "' + parserAlias + '" does not exist.');
}

return query;
return sql.type(typeAliases[parserAlias]);
};
};

sql.unnest = (
tuples: ReadonlyArray<readonly PrimitiveValueExpression[]>,
columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>,
): UnnestSqlToken => {
return {
columnTypes,
tuples,
type: UnnestToken,
sql.unnest = (
tuples: ReadonlyArray<readonly PrimitiveValueExpression[]>,
columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>,
): UnnestSqlToken => {
return {
columnTypes,
tuples,
type: UnnestToken,
};
};
};

export const createSqlTag = <T extends QueryResultRow = QueryResultRow>() => {
return sql as SqlTaggedTemplate<T>;
// @ts-expect-error TODO fix
return sql as SqlTaggedTemplate<typeof typeAliases, T>;
};
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -373,7 +373,7 @@ export type NamedAssignment = {
readonly [key: string]: ValueExpression,
};

export type SqlTaggedTemplate<Z extends QueryResultRow = QueryResultRow> = {
export type SqlTaggedTemplate<TypeAliases extends Record<string, ZodTypeAny>, Z extends QueryResultRow = QueryResultRow> = {
<U extends QueryResultRow = Z>(template: TemplateStringsArray, ...values: ValueExpression[]): SqlSqlToken<U>,
array: (
values: readonly PrimitiveValueExpression[],
@@ -389,6 +389,7 @@ export type SqlTaggedTemplate<Z extends QueryResultRow = QueryResultRow> = {
literalValue: (value: string) => SqlSqlToken,
timestamp: (date: Date) => TimestampSqlToken,
type: <Y extends ZodTypeAny>(parser: Y) => (template: TemplateStringsArray, ...values: ValueExpression[]) => SqlSqlToken<Y>,
typeAlias: <Y extends keyof TypeAliases>(typeAlias: Y) => (template: TemplateStringsArray, ...values: ValueExpression[]) => SqlSqlToken<TypeAliases[Y]>,
unnest: (
// Value might be ReadonlyArray<ReadonlyArray<PrimitiveValueExpression>>,
// or it can be infinitely nested array, e.g.
2 changes: 1 addition & 1 deletion test/dts.ts
Original file line number Diff line number Diff line change
@@ -408,7 +408,7 @@ const sqlTypes = async () => {
};

const createSqlTagTypes = () => {
const sqlTag: SqlTaggedTemplate = createSqlTag();
const sqlTag = createSqlTag();

sqlTag`
SELECT 1;
18 changes: 0 additions & 18 deletions test/slonik/templateTags/sql/sql.ts
Original file line number Diff line number Diff line change
@@ -4,9 +4,6 @@ import anyTest, {
import {
ROARR,
} from 'roarr';
import {
z,
} from 'zod';
import {
createSqlTag,
} from '../../../../src/factories/createSqlTag';
@@ -112,18 +109,3 @@ test('the sql property is immutable', (t) => {
query.sql = 'SELECT 2';
});
});

test('describes zod object associated with the query', (t) => {
const zodObject = z.object({
id: z.number(),
});

const query = sql.type(zodObject)`
SELECT 1 id
`;

t.is(query.parser, zodObject);

// @ts-expect-error Accessing a private property
t.is(query.parser._def?.typeName, 'ZodObject');
});
21 changes: 21 additions & 0 deletions test/slonik/templateTags/sql/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import test from 'ava';
import {
z,
} from 'zod';
import {
createSqlTag,
} from '../../../../src/factories/createSqlTag';

const sql = createSqlTag();

test('describes zod object associated with the query', (t) => {
const zodObject = z.object({
id: z.number(),
});

const query = sql.type(zodObject)`
SELECT 1 id
`;

t.is(query.parser, zodObject);
});
42 changes: 42 additions & 0 deletions test/slonik/templateTags/sql/typeAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import anyTest, {
type TestFn,
} from 'ava';
import {
ROARR,
} from 'roarr';
import {
z,
} from 'zod';
import {
createSqlTag,
} from '../../../../src/factories/createSqlTag';

const test = anyTest as TestFn<{
logs: unknown[],
}>;

test.beforeEach((t) => {
t.context.logs = [];

ROARR.write = (message) => {
t.context.logs.push(JSON.parse(message));
};
});

test('describes zod object associated with the query', (t) => {
const typeAliases = {
id: z.object({
id: z.number(),
}),
};

const sql = createSqlTag({
typeAliases,
});

const query = sql.typeAlias('id')`
SELECT 1 id
`;

t.is(query.parser, typeAliases.id);
});
11 changes: 10 additions & 1 deletion test/types.ts
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import {
} from 'zod';
import {
createPool,
sql,
createSqlTag,
} from '../src';
import {
type PrimitiveValueExpression,
@@ -31,6 +31,12 @@ export const queryMethods = async (): Promise<void> => {
foo: string,
};

const sql = createSqlTag({
typeAliases: {
row: ZodRow,
},
});

// parser
const parser = sql.type(ZodRow)``.parser;
expectTypeOf(parser).toEqualTypeOf<typeof ZodRow>();
@@ -48,6 +54,9 @@ export const queryMethods = async (): Promise<void> => {
const anyZodTypedQuery = await client.any(sql.type(ZodRow)``);
expectTypeOf(anyZodTypedQuery).toEqualTypeOf<readonly Row[]>();

const anyZodAliasQuery = await client.any(sql.typeAlias('row')``);
expectTypeOf(anyZodAliasQuery).toEqualTypeOf<readonly Row[]>();

// anyFirst
const anyFirst = await client.anyFirst(sql``);
expectTypeOf(anyFirst).toEqualTypeOf<readonly PrimitiveValueExpression[]>();