Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added middleware() API, types & tests #60

Merged
merged 2 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>>;
fhur marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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,
fhur marked this conversation as resolved.
Show resolved Hide resolved
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;
fhur marked this conversation as resolved.
Show resolved Hide resolved

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>({
fhur marked this conversation as resolved.
Show resolved Hide resolved
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