diff --git a/.changeset/strong-plants-wink.md b/.changeset/strong-plants-wink.md new file mode 100644 index 00000000..505d0994 --- /dev/null +++ b/.changeset/strong-plants-wink.md @@ -0,0 +1,5 @@ +--- +"@slonik/dataloaders": minor +--- + +omit internal slonikqueryindex diff --git a/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.test.ts b/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.test.ts index a87e4906..9e7a7d1c 100644 --- a/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.test.ts +++ b/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.test.ts @@ -36,11 +36,13 @@ const getInfo = ( const PersonConnectionLoader = createConnectionLoaderClass({ query: sql.type( - z.object({ - id: z.number(), - name: z.string(), - uid: z.string(), - }), + z + .object({ + id: z.number(), + name: z.string(), + uid: z.string(), + }) + .strict(), )` SELECT id, @@ -353,7 +355,7 @@ describe('createConnectionLoaderClass', () => { where: ({ name }) => sql.fragment`${name} = 'ccc'`, }); - expect(result.count).toEqual(2n); + expect(result.count).toEqual(2); }); it('gets the count without fetching edges', async () => { @@ -363,7 +365,7 @@ describe('createConnectionLoaderClass', () => { where: ({ name }) => sql.fragment`${name} = 'ccc'`, }); - expect(result.count).toEqual(2n); + expect(result.count).toEqual(2); expect(result.edges.length).toEqual(0); }); @@ -380,9 +382,9 @@ describe('createConnectionLoaderClass', () => { }), ]); - expect(results[0].count).toEqual(2n); + expect(results[0].count).toEqual(2); expect(results[0].edges.length).toEqual(0); - expect(results[1].count).toEqual(2n); + expect(results[1].count).toEqual(2); expect(results[1].edges.length).toEqual(0); }); @@ -401,7 +403,7 @@ describe('createConnectionLoaderClass', () => { ]); expect(results[0].count).toEqual(0); - expect(results[1].count).toEqual(2n); + expect(results[1].count).toEqual(2); }); it('gets the edges without fetching edges', async () => { @@ -492,21 +494,30 @@ describe('createConnectionLoaderClass (with validation)', () => { } }); + it('loads all records with row validation', async () => { + const loader = new PersonConnectionLoader(pool); + const result = await loader.load({}); + + expect(result.edges).toHaveLength(10); + }); + it('fails with schema validation error', async () => { const BadConnectionLoader = createConnectionLoaderClass({ query: sql.type( z.object({ - id: z.number(), + id: z.string(), uid: z.string(), }), )` SELECT - * + id, + uid FROM person `, }); const loader = new BadConnectionLoader(pool); + await expect(loader.load({})).rejects.toThrowError(SchemaValidationError); }); }); diff --git a/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.ts b/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.ts index 63a55f2c..22bffcdc 100644 --- a/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.ts +++ b/packages/slonik-dataloaders/src/factories/createConnectionLoaderClass.ts @@ -17,7 +17,7 @@ import { sql, type SqlToken, } from 'slonik'; -import { z, type ZodTypeAny } from 'zod'; +import { type AnyZodObject, z, type ZodTypeAny } from 'zod'; type DataLoaderKey = { cursor?: string | null; @@ -87,7 +87,7 @@ export const createConnectionLoaderClass = (config: { countQueries.push( sql.unsafe`( -- @count-query - SELECT count(*) count + SELECT count(*)::int4 count FROM ( ${query} ) ${sql.identifier([TABLE_ALIAS])} @@ -192,12 +192,11 @@ export const createConnectionLoaderClass = (config: { } } - let edgeSchema: ZodTypeAny = z.any(); + let edgeSchema: AnyZodObject = z.object({}); if ('shape' in query.parser) { edgeSchema = z .object({ - slonikqueryindex: z.number(), [SORT_COLUMN_ALIAS]: z.array(z.any()), // eslint-disable-next-line @typescript-eslint/no-explicit-any ...(query.parser as any).shape, @@ -207,7 +206,6 @@ export const createConnectionLoaderClass = (config: { const countSchema = z.object({ count: z.number(), - slonikqueryindex: z.number(), }); const [edgeResults, countResults] = await Promise.all([ @@ -249,6 +247,7 @@ export const createConnectionLoaderClass = (config: { while (true) { const value = record[SORT_COLUMN_ALIAS]?.[index]; + if (value === undefined) { break; } else { diff --git a/packages/slonik-dataloaders/src/utilities/batchQueries.ts b/packages/slonik-dataloaders/src/utilities/batchQueries.ts index a4250dee..b3455774 100644 --- a/packages/slonik-dataloaders/src/utilities/batchQueries.ts +++ b/packages/slonik-dataloaders/src/utilities/batchQueries.ts @@ -1,5 +1,5 @@ import { type CommonQueryMethods, type QuerySqlToken, sql } from 'slonik'; -import { type z, type ZodTypeAny } from 'zod'; +import { type AnyZodObject, z } from 'zod'; /** * Uses UNION to batch multiple queries that have the same shape. @@ -9,7 +9,7 @@ import { type z, type ZodTypeAny } from 'zod'; * This approach also has the benefit that it is compatible with * Slonik interceptors that validate the shape of the result set. */ -export const batchQueries = async ( +export const batchQueries = async ( pool: CommonQueryMethods, zodSchema: T, queries: readonly QuerySqlToken[], @@ -18,7 +18,9 @@ export const batchQueries = async ( return []; } - const results = await pool.any(sql.type(zodSchema)` + const results = await pool.any(sql.type( + zodSchema.extend({ slonikqueryindex: z.string() }), + )` ${sql.join( queries.map((query, index) => { return sql.fragment` @@ -33,8 +35,13 @@ export const batchQueries = async ( `); return queries.map((query, index) => { - return results.filter( - (result) => result.slonikqueryindex === String(index), - ); + return results + .filter((result) => result.slonikqueryindex === String(index)) + .map((result) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { slonikqueryindex, ...rest } = result; + + return rest; + }); }); }; diff --git a/packages/slonik/src/routines/executeQuery.ts b/packages/slonik/src/routines/executeQuery.ts index c099ac73..c9e8d4bd 100644 --- a/packages/slonik/src/routines/executeQuery.ts +++ b/packages/slonik/src/routines/executeQuery.ts @@ -352,9 +352,9 @@ export const executeQuery = async ( const { fields } = result; const rows: QueryResultRow[] = await Promise.all( - result.rows.map((row) => - transformRow(executionContext, actualQuery, row, fields), - ), + result.rows.map((row) => { + return transformRow(executionContext, actualQuery, row, fields); + }), ); result = {