Skip to content

Commit

Permalink
added middleware API, types & tests
Browse files Browse the repository at this point in the history
Signed-off-by: Jim Ezesinachi <ezesinachijim@gmail.com>
  • Loading branch information
jimezesinachi committed Oct 14, 2024
1 parent 4209cf9 commit 43d03a2
Show file tree
Hide file tree
Showing 17 changed files with 226 additions and 41 deletions.
88 changes: 73 additions & 15 deletions packages/backend/src/QueryEngine.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Pool } from 'pg';
import { Query, QueryResult, Table } from '@synthql/queries';
import { composeQuery } from './execution/executors/PgExecutor/composeQuery';
import { QueryPlan, collectLast } from '.';
import { QueryExecutor } from './execution/types';
import { QueryProvider } from './QueryProvider';
import { execute } from './execution/execute';
import { QueryExecutor } from './execution/types';
import { QueryProviderExecutor } from './execution/executors/QueryProviderExecutor';
import { Middleware } from './execution/middleware';
import { PgExecutor } from './execution/executors/PgExecutor';
import { QueryProviderExecutor } from './execution/executors/QueryProviderExecutor';
import { composeQuery } from './execution/executors/PgExecutor/composeQuery';
import { generateLast } from './util/generators/generateLast';
import { SynthqlError } from './SynthqlError';

Expand All @@ -30,6 +31,40 @@ export interface QueryEngineProps<DB> {
* e.g `SELECT version();`
*/
prependSql?: string;
/**
* A list of middlewares that you want to be used
* to transform any matching queries before execution
*
* e.g:
*
* ```ts
* // Create type/interface for context
* type UserRole = 'user' | 'admin' | 'super';
*
* interface Session {
* id: number;
* email: string;
* roles: UserRole[];
* isActive: boolean;
* }
*
* // Create middleware
* const restrictPaymentsByCustomer = middleware<Query<DB, 'payment'>, Session>({
* predicate: ({ query, context }) =>
* query.from === 'payment' &&
* context.roles.includes('user') &&
* context.isActive,
* transformQuery: ({ query, context }) => ({
* ...query,
* where: {
* ...query.where,
* customer_id: context.id,
* },
* }),
* });
* ```
*/
middlewares?: Array<Middleware<any, any>>;
/**
* A list of providers that you want to be used
* to execute your SynthQL queries against.
Expand Down Expand Up @@ -71,7 +106,8 @@ export class QueryEngine<DB> {
private pool: Pool;
private schema: string;
private prependSql?: string;
private executors: Array<QueryExecutor> = [];
private middlewares: Array<Middleware>;
private executors: Array<QueryExecutor>;

constructor(config: QueryEngineProps<DB>) {
this.schema = config.schema ?? 'public';
Expand All @@ -82,6 +118,7 @@ export class QueryEngine<DB> {
connectionString: config.url,
max: 10,
});
this.middlewares = config.middlewares ?? [];

const qpe = new QueryProviderExecutor(config.providers ?? []);
this.executors = [
Expand All @@ -96,8 +133,13 @@ export class QueryEngine<DB> {
];
}

execute<TTable extends Table<DB>, TQuery extends Query<DB, TTable>>(
execute<
TTable extends Table<DB>,
TQuery extends Query<DB, TTable>,
TContext,
>(
query: TQuery,
context?: TContext,
opts?: {
/**
* The name of the database schema to execute
Expand All @@ -113,7 +155,23 @@ export class QueryEngine<DB> {
returnLastOnly?: boolean;
},
): AsyncGenerator<QueryResult<DB, TQuery>> {
const gen = execute<DB, TQuery>(query, {
let transformedQuery: any = query;

for (const middleware of this.middlewares) {
if (
middleware.predicate({
query,
context: context,
})
) {
transformedQuery = middleware.transformQuery({
query: transformedQuery,
context: context,
});
}
}

const gen = execute<DB, TQuery>(transformedQuery as TQuery, {
executors: this.executors,
defaultSchema: opts?.schema ?? this.schema,
prependSql: this.prependSql,
Expand All @@ -129,8 +187,10 @@ export class QueryEngine<DB> {
async executeAndWait<
TTable extends Table<DB>,
TQuery extends Query<DB, TTable>,
TContext,
>(
query: TQuery,
context?: TContext,
opts?: {
/**
* The name of the database schema to execute
Expand All @@ -141,18 +201,15 @@ export class QueryEngine<DB> {
schema?: string;
},
): Promise<QueryResult<DB, TQuery>> {
return await collectLast(
generateLast(
execute<DB, TQuery>(query, {
executors: this.executors,
defaultSchema: opts?.schema ?? this.schema,
prependSql: this.prependSql,
}),
),
return collectLast(
this.execute(query, context, {
schema: opts?.schema ?? this.schema,
returnLastOnly: true,
}),
);
}

compile<T>(query: T extends Query<DB, infer TTable> ? T : never): {
compile<T>(query: T extends Query<DB> ? T : never): {
sql: string;
params: any[];
} {
Expand All @@ -178,6 +235,7 @@ export class QueryEngine<DB> {

try {
const result = await this.pool.query(explainQuery, params);

return result.rows[0]['QUERY PLAN'][0];
} catch (err) {
throw SynthqlError.createSqlExecutionError({
Expand Down
61 changes: 61 additions & 0 deletions packages/backend/src/execution/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { test, describe, expect } from 'vitest';
import { DB, from } from '../tests/generated';
import { Query } from '@synthql/queries';
import { middleware } from './middleware';
import { createQueryEngine } from '../tests/queryEngine';

// Create type/interface for context
type UserRole = 'user' | 'admin' | 'super';
interface Session {
id: number;
email: string;
roles: UserRole[];
isActive: boolean;
}

// Create middleware
const restrictPaymentsByCustomer = middleware<Query<DB, 'payment'>, Session>({
predicate: ({ query, context }) =>
query?.from === 'payment' &&
context?.roles?.includes('user') &&
context?.isActive,
transformQuery: ({ query, context }) => ({
...query,
where: {
...query.where,
customer_id: context.id,
},
}),
});

describe('createExpressSynthqlHandler', async () => {
test('1', async () => {
const queryEngine = createQueryEngine([restrictPaymentsByCustomer]);

// Create context
// This would 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,
};

// Create base query
const q = from('payment').one();

const queryWithContextManuallyAdded = from('payment')
.where({
customer_id: context.id,
})
.one();

const result = await queryEngine.executeAndWait(q, context);

const resultFromQueryWithContextManuallyAdded =
await queryEngine.executeAndWait(queryWithContextManuallyAdded);

expect(result).toEqual(resultFromQueryWithContextManuallyAdded);
});
});
41 changes: 41 additions & 0 deletions packages/backend/src/execution/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export interface Middleware<TQuery = unknown, TContext = unknown> {
predicate: ({
query,
context,
}: {
query: TQuery;
context: TContext;
}) => boolean;
transformQuery: ({
query,
context,
}: {
query: TQuery;
context: TContext;
}) => TQuery;
}

export function middleware<TQuery = unknown, TContext = unknown>({
predicate,
transformQuery,
}: {
predicate: ({
query,
context,
}: {
query: TQuery;
context: TContext;
}) => boolean;
transformQuery: ({
query,
context,
}: {
query: TQuery;
context: TContext;
}) => TQuery;
}): Middleware<TQuery, TContext> {
return {
predicate,
transformQuery,
};
}
1 change: 1 addition & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { collectLast } from './util/generators/collectLast';
export { composeQuery } from './execution/executors/PgExecutor/composeQuery';
export { middleware } from './execution/middleware';
export type * from './types/QueryPlan';
export * from './QueryEngine';
export * from './SynthqlError';
26 changes: 18 additions & 8 deletions packages/backend/src/tests/benchmarks/bench.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { col } from '@synthql/queries';
import { describe, test } from 'vitest';
import { col } from '@synthql/queries';
import { collectLast } from '../..';
import { from } from '../generated';
import { queryEngine } from '../queryEngine';
import { createQueryEngine } from '../queryEngine';
import Benchmark from 'benchmark';
import fs from 'fs';
import path from 'path';

describe('Benchmark tests', () => {
test(`Find matching rows`, async () => {
const queryEngine = createQueryEngine();

const suite = new Benchmark.Suite();
const lines: Array<string> = [];

Expand All @@ -17,9 +19,13 @@ 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(
Expand Down Expand Up @@ -61,9 +67,13 @@ describe('Benchmark tests', () => {
.one();

await collectLast(
queryEngine.execute(q, {
returnLastOnly: true,
}),
queryEngine.execute(
q,
{},
{
returnLastOnly: true,
},
),
);
},
)
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/tests/e2e/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { collectLast } from '../..';
import { DB } from '../generated';
import { sql } from '../postgres';
import { findActorById, findCityById, from, movie } from '../queries';
import { queryEngine } from '../queryEngine';
import { createQueryEngine } from '../queryEngine';

const queryEngine = createQueryEngine();

describe('select', () => {
function run<TTable extends Table<DB>, T extends Query<DB, TTable>>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { test } from '@fast-check/vitest';
import { ArbitraryQueryBuilder } from '../arbitraries/ArbitraryQueryBuilder';
import { queryEngine } from '../../queryEngine';
import { createQueryEngine } from '../../queryEngine';
import { describe, expect } from 'vitest';

const queryBuilder = ArbitraryQueryBuilder.fromPagila();
const queryEngine = createQueryEngine();

describe('No results', () => {
const numRuns = 1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { it } from '@fast-check/vitest';
import { describe, expect } from 'vitest';
import { Query } from '@synthql/queries';
import { DB, schema } from '../../generated';
import { pool, queryEngine } from '../../queryEngine';
import { pool, createQueryEngine } from '../../queryEngine';
import { arbitraryQuery } from '../arbitraries/arbitraryQuery';
import { getTableRowsByTableName } from '../getTableRowsByTableName';

const queryEngine = createQueryEngine();

describe('cardinalityMany', async () => {
const validWhereArbitraryQuery = arbitraryQuery<DB>({
schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { it } from '@fast-check/vitest';
import { describe, expect } from 'vitest';
import { Query } from '@synthql/queries';
import { DB, schema } from '../../generated';
import { pool, queryEngine } from '../../queryEngine';
import { pool, createQueryEngine } from '../../queryEngine';
import { arbitraryQuery } from '../arbitraries/arbitraryQuery';
import { getTableRowsByTableName } from '../getTableRowsByTableName';

const queryEngine = createQueryEngine();

describe('cardinalityMaybe', async () => {
const validWhereArbitraryQuery = arbitraryQuery<DB>({
schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { it } from '@fast-check/vitest';
import { describe, expect } from 'vitest';
import { Query } from '@synthql/queries';
import { DB, schema } from '../../generated';
import { pool, queryEngine } from '../../queryEngine';
import { pool, createQueryEngine } from '../../queryEngine';
import { arbitraryQuery } from '../arbitraries/arbitraryQuery';
import { getTableRowsByTableName } from '../getTableRowsByTableName';
import { SynthqlError } from '../../../SynthqlError';

const queryEngine = createQueryEngine();

describe('cardinalityOne', async () => {
const validWhereArbitraryQuery = arbitraryQuery<DB>({
schema,
Expand Down
Loading

0 comments on commit 43d03a2

Please sign in to comment.