diff --git a/packages/backend/src/QueryEngine.ts b/packages/backend/src/QueryEngine.ts index 33bf6f1..e7b0b49 100644 --- a/packages/backend/src/QueryEngine.ts +++ b/packages/backend/src/QueryEngine.ts @@ -13,29 +13,37 @@ import { SynthqlError } from './SynthqlError'; export interface QueryEngineProps { /** - * The database connection string e.g. `postgresql://user:password@localhost:5432/db`. + * The database connection string. * - * If you use this option, SynthQL will create a conection pool for you internally. + * e.g. `postgresql://user:password@localhost:5432/db`. + * + * If you use this option, SynthQL will create + * a conection pool for you internally. */ url?: string; /** * The name of the database schema to * execute your SynthQL queries against. * - * e.g `public` + * e.g. `public` */ schema?: string; /** - * An optional SQL statement that will be sent before every SynthQL query. + * An optional SQL statement that will + * be sent before every SynthQL query. + * + * e.g.: * - * e.g `SELECT version();` + * ```sql + * SELECT version(); + * ``` */ prependSql?: string; /** - * A list of middlewares that you want to be used - * to transform any matching queries before execution + * A list of middlewares that you want to be used to + * transform any matching queries, before execution. * - * e.g: + * e.g.: * * ```ts * // Create type/interface for context @@ -46,10 +54,11 @@ export interface QueryEngineProps { * email: string; * roles: UserRole[]; * isActive: boolean; - * } + * }; * * // Create middleware - * const restrictPaymentsByCustomer = middleware, Session>({ + * const restrictPaymentsByCustomer = + * middleware, Session>({ * predicate: ({ query, context }) => * query.from === 'payment' && * context.roles.includes('user') && @@ -69,7 +78,7 @@ export interface QueryEngineProps { * A list of providers that you want to be used * to execute your SynthQL queries against. * - * e.g: + * e.g.: * * ```ts * const films = [{ @@ -82,17 +91,21 @@ export interface QueryEngineProps { * * const filmProvider = { * table: 'film', - * execute: async ({ film_id: filmIds }): Promise<{ film_id: number }[]> => { - * return films.filter((f) => filmIds.includes(f.film_id)); + * execute: async ({ film_id: filmIds }): + * Promise<{ film_id: number }[]> => { + * return films.filter((f) => + * filmIds.includes(f.film_id)); * }, * }; * ``` */ providers?: Array>>; /** - * The connection pool to which the executor will send SQL queries to. + * The connection pool to which the + * executor will send SQL queries to. * - * You can use this instead of passing a connection string. + * You can use this instead of + * passing a connection string. */ pool?: Pool; @@ -139,11 +152,11 @@ export class QueryEngine { TContext, >( query: TQuery, - context?: TContext, opts?: { + context?: TContext; /** - * The name of the database schema to execute - * your SynthQL query against + * The name of the database schema to + * execute your SynthQL query against * * e.g `public` */ @@ -161,12 +174,12 @@ export class QueryEngine { if ( middleware.predicate({ query, - context: context, + context: opts?.context, }) ) { transformedQuery = middleware.transformQuery({ query: transformedQuery, - context: context, + context: opts?.context, }); } } @@ -190,19 +203,49 @@ export class QueryEngine { TContext, >( query: TQuery, - context?: TContext, opts?: { /** - * The name of the database schema to execute - * your SynthQL query against + * When using middlewares (via the `QueryEngine` options), + * pass the data that should be used to transform + * the query, via this option * - * e.g `public` + * e.g.: + * + * ```ts + * // Create type/interface for context + * type UserRole = 'user' | 'admin' | 'super'; + * + * interface Session { + * id: number; + * email: string; + * roles: UserRole[]; + * isActive: boolean; + * }; + * + * // Create context + * // This would usually be an object generated from a server + * // request handler (e.g a parsed cookie/token) + * const context: Session = { + * id: 1, + * email: 'user@example.com', + * roles: ['user', 'admin', 'super'], + * isActive: true, + * }; + * ``` + */ + context?: TContext; + /** + * The name of the database schema to + * execute your SynthQL query against + * + * e.g. `public` */ schema?: string; }, ): Promise> { return collectLast( - this.execute(query, context, { + this.execute(query, { + context: opts?.context, schema: opts?.schema ?? this.schema, returnLastOnly: true, }), @@ -235,7 +278,6 @@ export class QueryEngine { try { const result = await this.pool.query(explainQuery, params); - return result.rows[0]['QUERY PLAN'][0]; } catch (err) { throw SynthqlError.createSqlExecutionError({ diff --git a/packages/backend/src/execution/middleware.test.ts b/packages/backend/src/execution/middleware.test.ts index 1b0909a..7d4a408 100644 --- a/packages/backend/src/execution/middleware.test.ts +++ b/packages/backend/src/execution/middleware.test.ts @@ -33,7 +33,7 @@ describe('createExpressSynthqlHandler', async () => { const queryEngine = createQueryEngine([restrictPaymentsByCustomer]); // Create context - // This would be an object generated from a server + // This would usually be an object generated from a server // request handler (e.g a parsed cookie/token) const context: Session = { id: 1, @@ -51,7 +51,7 @@ describe('createExpressSynthqlHandler', async () => { }) .one(); - const result = await queryEngine.executeAndWait(q, context); + const result = await queryEngine.executeAndWait(q, { context }); const resultFromQueryWithContextManuallyAdded = await queryEngine.executeAndWait(queryWithContextManuallyAdded); diff --git a/packages/backend/src/tests/benchmarks/bench.test.ts b/packages/backend/src/tests/benchmarks/bench.test.ts index f0b394b..2646932 100644 --- a/packages/backend/src/tests/benchmarks/bench.test.ts +++ b/packages/backend/src/tests/benchmarks/bench.test.ts @@ -19,13 +19,9 @@ describe('Benchmark tests', () => { const q = from('actor').where({ actor_id: 1 }).one(); await collectLast( - queryEngine.execute( - q, - {}, - { - returnLastOnly: true, - }, - ), + queryEngine.execute(q, { + returnLastOnly: true, + }), ); }) .add( @@ -67,13 +63,9 @@ describe('Benchmark tests', () => { .one(); await collectLast( - queryEngine.execute( - q, - {}, - { - returnLastOnly: true, - }, - ), + queryEngine.execute(q, { + returnLastOnly: true, + }), ); }, ) diff --git a/packages/handler-express/src/createExpressSynthqlHandler.ts b/packages/handler-express/src/createExpressSynthqlHandler.ts index ff19a1c..88fd7da 100644 --- a/packages/handler-express/src/createExpressSynthqlHandler.ts +++ b/packages/handler-express/src/createExpressSynthqlHandler.ts @@ -93,7 +93,7 @@ async function tryExecuteQuery( query: any, returnLastOnly: boolean, ) { - return queryEngine.execute(query, {}, { returnLastOnly }); + return queryEngine.execute(query, { returnLastOnly }); } async function writeBody( diff --git a/packages/handler-next/src/createNextSynthqlHandler.ts b/packages/handler-next/src/createNextSynthqlHandler.ts index a682db7..db85be1 100644 --- a/packages/handler-next/src/createNextSynthqlHandler.ts +++ b/packages/handler-next/src/createNextSynthqlHandler.ts @@ -86,7 +86,7 @@ async function tryExecuteQuery( query: any, returnLastOnly: boolean, ) { - return queryEngine.execute(query, {}, { returnLastOnly }); + return queryEngine.execute(query, { returnLastOnly }); } async function writeBody( @@ -109,7 +109,6 @@ async function writeBody( return new NextResponse(stream, { // This is a streaming request, so albeit // counterintuitively, we always need to return 2xx - status: 200, headers: { 'Content-Type': 'application/x-ndjson' }, }); @@ -130,7 +129,6 @@ async function writeBody( // The `e` can be of any type, but in case its an error, // we want to preserve the stack trace and any other // information that might be useful for debugging - const error = SynthqlError.createResponseStreamingError({ error: e, query, @@ -138,7 +136,6 @@ async function writeBody( // We need to catch errors here and write them to the streaming response // We can't throw them because that would break the stream - return new NextResponse( JSON.stringify({ type: error.type,