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

Upgrade to PostGraphile V5 #355

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
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
@@ -1,4 +1,4 @@
import {

Check failure on line 1 in @app/lib/src/GraphileApolloLink.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Run autofix to sort these imports!

Check failure on line 1 in @app/lib/src/GraphileApolloLink.ts

View workflow job for this annotation

GitHub Actions / build

Run autofix to sort these imports!
ApolloLink,
FetchResult,
NextLink,
Expand All @@ -6,8 +6,10 @@
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 @@
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 @@
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 @@
}
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";

Check failure on line 1 in @app/server/__tests__/helpers.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unable to resolve path to module '@dataplan/pg/adaptors/pg'

Check failure on line 1 in @app/server/__tests__/helpers.ts

View workflow job for this annotation

GitHub Actions / build

Unable to resolve path to module '@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 @@
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 @@
// 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 @@
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 @@
) => 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 @@
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
Loading