Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion @app/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Pool, PoolClient } from "pg";

const pools = {};
const pools: { [key: string]: Pool } = {};

if (!process.env.TEST_DATABASE_URL) {
throw new Error("Cannot run tests without a TEST_DATABASE_URL");
Expand Down
2 changes: 1 addition & 1 deletion @app/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@types/react": "18.0.28",
"antd": "5.2.3",
"dayjs": "^1.11.7",
"graphql": "^15.8.0",
"graphql": "^16.1.0-experimental-stream-defer.6",
"lodash": "^4.17.21",
"net": "^1.0.2",
"next": "^13.2.3",
Expand Down
4 changes: 2 additions & 2 deletions @app/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"@apollo/client": "3.4.17",
"graphql": "^15.8.0",
"graphql": "^16.1.0-experimental-stream-defer.6",
"graphql-ws": "^5.11.3",
"next": "^13.2.3",
"next-with-apollo": "^5.3.0",
Expand All @@ -25,7 +25,7 @@
"cross-env": "^7.0.3",
"express": "^4.18.2",
"jest": "^29.4.3",
"postgraphile": "^4.13.0",
"postgraphile": "^5.0.0-beta.2",
"typescript": "^5.0.0-beta"
}
}
47 changes: 26 additions & 21 deletions @app/lib/src/GraphileApolloLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
Operation,
} from "@apollo/client";
import { Request, Response } from "express";
import { execute, getOperationAST } from "graphql";
import { HttpRequestHandler } from "postgraphile";
import { execute, hookArgs, isAsyncIterable } from "grafast";
import type {} from "postgraphile/grafserv/express/v4";
import { getOperationAST } from "graphql";
import type { PostGraphileInstance } from "postgraphile";

export interface GraphileApolloLinkInterface {
/** The request object. */
Expand All @@ -17,10 +19,7 @@ export interface GraphileApolloLinkInterface {
res: Response;

/** The instance of the express middleware returned by calling `postgraphile()` */
postgraphileMiddleware: HttpRequestHandler<Request, Response>;

/** An optional rootValue to use inside resolvers. */
rootValue?: any;
pgl: PostGraphileInstance;
}

/**
Expand All @@ -36,7 +35,7 @@ export class GraphileApolloLink extends ApolloLink {
operation: Operation,
_forward?: NextLink
): Observable<FetchResult> | null {
const { postgraphileMiddleware, req, res, rootValue } = this.options;
const { pgl, req, res } = this.options;
return new Observable((observer) => {
(async () => {
try {
Expand All @@ -53,22 +52,28 @@ export class GraphileApolloLink extends ApolloLink {
}
return;
}
const schema = await postgraphileMiddleware.getGraphQLSchema();
const data =
await postgraphileMiddleware.withPostGraphileContextFromReqRes(
const schema = await pgl.getSchema();
const args = {
schema,
document,
variableValues,
operationName,
};
await hookArgs(args, pgl.getResolvedPreset(), {
node: {
req,
res,
{},
(context) =>
execute(
schema,
document,
rootValue || {},
context,
variableValues,
operationName
)
);
},
expressv4: {
req,
res,
},
});
const data = await execute(args);
if (isAsyncIterable(data)) {
data.return?.();
throw new Error("Iterable not supported by GraphileApolloLink");
}
if (!observer.closed) {
observer.next(data);
observer.complete();
Expand Down
2 changes: 1 addition & 1 deletion @app/lib/src/withApollo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function makeServerSideLink(req: any, res: any) {
return new GraphileApolloLink({
req,
res,
postgraphileMiddleware: req.app.get("postgraphileMiddleware"),
pgl: req.app.get("pgl"),
});
}

Expand Down
191 changes: 95 additions & 96 deletions @app/server/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Request, Response } from "express";
import { ExecutionResult, graphql, GraphQLSchema } from "graphql";
import { Pool, PoolClient } from "pg";
import { makeWithPgClientViaPgClientAlreadyInTransaction } from "@dataplan/pg/adaptors/pg";
import { execute, hookArgs } from "grafast";
import {
createPostGraphileSchema,
PostGraphileOptions,
withPostGraphileContext,
} from "postgraphile";
ExecutionArgs,
ExecutionResult,
GraphQLSchema,
parse,
validate,
} from "graphql";
import { Pool, PoolClient } from "pg";
import { postgraphile, PostGraphileInstance } from "postgraphile";

import {
createSession,
createUsers,
poolFromUrl,
} from "../../__tests__/helpers";
import { getPostGraphileOptions } from "../src/graphile.config";
import { getPreset } from "../src/graphile.config";

export * from "../../__tests__/helpers";

Expand All @@ -22,8 +25,8 @@ export async function createUserAndLogIn() {
const pool = poolFromUrl(process.env.TEST_DATABASE_URL!);
const client = await pool.connect();
try {
const [user] = await createUsers(pool, 1, true);
const session = await createSession(pool, user.id);
const [user] = await createUsers(client, 1, true);
const session = await createSession(client, user.id);
return { user, session };
} finally {
client.release();
Expand Down Expand Up @@ -96,7 +99,7 @@ export function sanitize(json: any): any {
// Contains the PostGraphile schema and rootPgPool
interface ICtx {
rootPgPool: Pool;
options: PostGraphileOptions<Request, Response>;
pgl: PostGraphileInstance;
schema: GraphQLSchema;
}
let ctx: ICtx | null = null;
Expand All @@ -106,17 +109,14 @@ export const setup = async () => {
connectionString: process.env.TEST_DATABASE_URL,
});

const options = getPostGraphileOptions({ rootPgPool });
const schema = await createPostGraphileSchema(
rootPgPool,
"app_public",
options
);
const preset = getPreset({ rootPgPool, authPgPool: rootPgPool });
const pgl = postgraphile(preset);
const schema = await pgl.getSchema();

// Store the context
ctx = {
rootPgPool,
options,
pgl,
schema,
};
};
Expand Down Expand Up @@ -146,9 +146,10 @@ export const runGraphQLQuery = async function runGraphQLQuery(
) => void | ExecutionResult | Promise<void | ExecutionResult> = () => {} // Place test assertions in this function
) {
if (!ctx) throw new Error("No ctx!");
const { schema, rootPgPool, options } = ctx;
const { schema, rootPgPool, pgl } = ctx;
const resolvedPreset = pgl.getResolvedPreset();
const req = new MockReq({
url: options.graphqlRoute || "/graphql",
url: resolvedPreset.grafserv?.graphqlPath || "/graphql",
method: "POST",
headers: {
Accept: "application/json",
Expand All @@ -159,92 +160,90 @@ export const runGraphQLQuery = async function runGraphQLQuery(
const res: any = { req };
req.res = res;

const {
pgSettings: pgSettingsGenerator,
additionalGraphQLContextFromRequest,
} = options;
const pgSettings =
(typeof pgSettingsGenerator === "function"
? await pgSettingsGenerator(req)
: pgSettingsGenerator) || {};
let checkResult: ExecutionResult | void;
const document = parse(query);
const errors = validate(schema, document);
if (errors.length > 0) {
throw errors[0];
}
const args: ExecutionArgs = {
schema,
document,
contextValue: {
__TESTING: true,
},
variableValues: variables,
};
await hookArgs(args, resolvedPreset, {
node: { req, res },
expressv4: { req, res },
});

// Because we're connected as the database owner, we should manually switch to
// the authenticator role
if (!pgSettings.role) {
pgSettings.role = process.env.DATABASE_AUTHENTICATOR;
const context = args.contextValue as Grafast.Context;
if (!context.pgSettings?.role) {
context.pgSettings = context.pgSettings ?? {};
context.pgSettings.role = process.env.DATABASE_AUTHENTICATOR as string;
}

await withPostGraphileContext(
{
...options,
pgPool: rootPgPool,
pgSettings,
pgForceTransaction: true,
},
async (context) => {
let checkResult;
const { pgClient } = context;
try {
// This runs our GraphQL query, passing the replacement client
const additionalContext = additionalGraphQLContextFromRequest
? await additionalGraphQLContextFromRequest(req, res)
: null;
const result = await graphql(
schema,
query,
null,
{
...context,
...additionalContext,
__TESTING: true,
},
variables
);
// Expand errors
if (result.errors) {
if (options.handleErrors) {
result.errors = options.handleErrors(result.errors);
} else {
// This does a similar transform that PostGraphile does to errors.
// It's not the same. Sorry.
result.errors = result.errors.map((rawErr) => {
const e = Object.create(rawErr);
Object.defineProperty(e, "originalError", {
value: rawErr.originalError,
enumerable: false,
});

if (e.originalError) {
Object.keys(e.originalError).forEach((k) => {
try {
e[k] = e.originalError[k];
} catch (err) {
// Meh.
}
});
const pgClient = await rootPgPool.connect();
try {
await pgClient.query("begin");

// Override withPgClient with a transactional version for the tests
const withPgClient = makeWithPgClientViaPgClientAlreadyInTransaction(
pgClient,
true
);
context.withPgClient = withPgClient;

const result = (await execute(args, resolvedPreset)) as ExecutionResult;
// Expand errors
if (result.errors) {
if (resolvedPreset.grafserv?.maskError) {
result.errors = result.errors.map(resolvedPreset.grafserv.maskError);
} else {
// This does a similar transform that PostGraphile does to errors.
// It's not the same. Sorry.
result.errors = result.errors.map((rawErr) => {
const e = Object.create(rawErr);
Object.defineProperty(e, "originalError", {
value: rawErr.originalError,
enumerable: false,
});

if (e.originalError) {
Object.keys(e.originalError).forEach((k) => {
try {
e[k] = e.originalError[k];
} catch (err) {
// Meh.
}
return e;
});
}
}

// This is were we call the `checker` so you can do your assertions.
// Also note that we pass the `replacementPgClient` so that you can
// query the data in the database from within the transaction before it
// gets rolled back.
checkResult = await checker(result, {
pgClient,
return e;
});
}
}

// You don't have to keep this, I just like knowing when things change!
expect(sanitize(result)).toMatchSnapshot();
// This is were we call the `checker` so you can do your assertions.
// Also note that we pass the `replacementPgClient` so that you can
// query the data in the database from within the transaction before it
// gets rolled back.
checkResult = await checker(result, {
pgClient,
});

return checkResult == null ? result : checkResult;
} finally {
// Rollback the transaction so no changes are written to the DB - this
// makes our tests fairly deterministic.
await pgClient.query("rollback");
}
// You don't have to keep this, I just like knowing when things change!
expect(sanitize(result)).toMatchSnapshot();

return checkResult == null ? result : checkResult;
} finally {
try {
await pgClient.query("rollback");
} finally {
pgClient.release();
}
);
}
};
4 changes: 4 additions & 0 deletions @app/server/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["."]
}
Loading