Skip to content

Commit

Permalink
Update KeystoneContext APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie committed Feb 14, 2021
1 parent 4035218 commit b55d425
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 133 deletions.
12 changes: 5 additions & 7 deletions packages-next/admin-ui/src/system/createAdminUIServer.ts
@@ -1,14 +1,14 @@
import url from 'url';
import next from 'next';
import express from 'express';
import type { KeystoneConfig, SessionImplementation, CreateContext } from '@keystone-next/types';
import type { KeystoneConfig, SessionStrategy, CreateContext } from '@keystone-next/types';

export const createAdminUIServer = async (
export const createAdminUIServer = async <SessionType>(
ui: KeystoneConfig['ui'],
createContext: CreateContext,
createContext: CreateContext<SessionType>,
dev: boolean,
projectAdminPath: string,
sessionImplementation?: SessionImplementation
sessionStrategy?: SessionStrategy<SessionType>
) => {
const app = next({ dev, dir: projectAdminPath });
const handle = app.getRequestHandler();
Expand All @@ -21,9 +21,7 @@ export const createAdminUIServer = async (
handle(req, res);
return;
}
const context = createContext({
sessionContext: await sessionImplementation?.createSessionContext(req, res, createContext),
});
const context = await createContext({ skipAccessControl: false, req, res, sessionStrategy });
const isValidSession = ui?.isAccessAllowed
? await ui.isAccessAllowed(context)
: context.session !== undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages-next/auth/src/index.ts
Expand Up @@ -163,7 +163,7 @@ export function createAuth<GeneratedListTypes extends BaseGeneratedListTypes>({
}

if (!session && initFirstItem) {
const count = await createContext({}).sudo().lists[listKey].count({});
const count = await (await createContext({})).sudo().lists[listKey].count({});
if (count === 0) {
if (pathname !== '/init') {
return {
Expand Down
173 changes: 127 additions & 46 deletions packages-next/keystone/src/lib/createContext.ts
@@ -1,7 +1,6 @@
import type { IncomingMessage } from 'http';
import { execute, GraphQLSchema, parse } from 'graphql';
import type {
SessionContext,
CreateContext,
KeystoneContext,
KeystoneGraphQLAPI,
BaseKeystone,
Expand All @@ -24,15 +23,95 @@ export function makeCreateContext({
getArgsByList[listKey] = getArgsFactory(list, graphQLSchema);
}

const createContext = ({
sessionContext,
skipAccessControl = false,
// This context creation code is somewhat fiddly, because some parts of the
// KeystoneContext object need to reference the object itself! In order to
// make this happen, we first prepare the object (_prepareContext), putting in
// placeholders for those parts which require self-binding. We then use this
// object in _bindToContext to fill in the blanks.

const _prepareContext = <SessionType>({
skipAccessControl,
req,
}: {
sessionContext?: SessionContext<any>;
skipAccessControl?: boolean;
req?: IncomingMessage;
} = {}): KeystoneContext => {
}: Parameters<CreateContext<SessionType>>[0]): KeystoneContext => ({
schemaName: 'public',
...(skipAccessControl ? skipAccessControlContext : accessControlContext),
totalResults: 0,
keystone,
// Only one of these will be available on any given context
// TODO: Capture that in the type
knex: keystone.adapter.knex,
mongoose: keystone.adapter.mongoose,
prisma: keystone.adapter.prisma,
maxTotalResults: keystone.queryLimits.maxTotalResults,
req,
// Note: These two fields let us use the server-side-graphql-client library.
// We may want to remove them once the updated itemAPI w/ resolveFields is available.
executeGraphQL: undefined,
gqlNames: (listKey: string) => keystone.lists[listKey].gqlNames,

// These properties need to refer to this object. We will bind them later (see _bindToContext)
session: undefined,
startSession: undefined,
endSession: undefined,
sudo: (() => {}) as KeystoneContext['sudo'],
exitSudo: (() => {}) as KeystoneContext['exitSudo'],
withSession: ((() => {}) as unknown) as KeystoneContext['withSession'],
graphql: {} as KeystoneContext['graphql'],
lists: {} as KeystoneContext['lists'],
});

const _bindToContext = <SessionType>({
contextToReturn,
skipAccessControl,
req,
res,
sessionStrategy,
session,
}: Parameters<CreateContext<SessionType>>[0] & {
contextToReturn: KeystoneContext;
session?: SessionType;
}) => {
// Bind session
contextToReturn.session = session;
if (sessionStrategy) {
Object.assign(contextToReturn, {
startSession: (data: SessionType) =>
sessionStrategy.start({ res: res!, data, context: contextToReturn }),
endSession: () => sessionStrategy.end({ req: req!, res: res!, context: contextToReturn }),
});
}

// Bind sudo/leaveSudo/withSession
// We want .sudo()/.leaveSudo()/.withSession() to be a synchronous functions, so rather
// than calling createContext, we follow the same steps, but skip the async session object
// creation, instead passing through the session object we already have, or the paramter
// for .withSession().
contextToReturn.sudo = () => {
const args = { skipAccessControl: true, req, res, sessionStrategy };
const contextToReturn = _prepareContext(args);
return _bindToContext({ contextToReturn, ...args, session });
};
contextToReturn.exitSudo = () => {
const args = { skipAccessControl: false, req, res, sessionStrategy };
const contextToReturn = _prepareContext(args);
return _bindToContext({ contextToReturn, ...args, session });
};
contextToReturn.withSession = session => {
const args = { skipAccessControl, req, res, sessionStrategy };
const contextToReturn = _prepareContext(args);
return _bindToContext({ contextToReturn, ...args, session });
};

// Bind items API
for (const [listKey, list] of Object.entries(keystone.lists)) {
contextToReturn.lists[listKey] = itemAPIForList(
list,
contextToReturn,
getArgsByList[listKey]
);
}

// Bind graphql API
const rawGraphQL: KeystoneGraphQLAPI<any>['raw'] = ({ query, context, variables }) => {
if (typeof query === 'string') {
query = parse(query);
Expand All @@ -53,45 +132,47 @@ export function makeCreateContext({
}
return result.data as Record<string, any>;
};
const itemAPI: Record<string, ReturnType<typeof itemAPIForList>> = {};
const contextToReturn: KeystoneContext = {
schemaName: 'public',
...(skipAccessControl ? skipAccessControlContext : accessControlContext),
lists: itemAPI,
totalResults: 0,
keystone,
// Only one of these will be available on any given context
// TODO: Capture that in the type
knex: keystone.adapter.knex,
mongoose: keystone.adapter.mongoose,
prisma: keystone.adapter.prisma,
graphql: {
createContext,
raw: rawGraphQL,
run: runGraphQL,
schema: graphQLSchema,
} as KeystoneGraphQLAPI<any>,
maxTotalResults: keystone.queryLimits.maxTotalResults,
sudo: () => createContext({ sessionContext, skipAccessControl: true, req }),
exitSudo: () => createContext({ sessionContext, skipAccessControl: false, req }),
withSession: session =>
createContext({
sessionContext: { ...sessionContext, session } as SessionContext<any>,
skipAccessControl,
req,
}),
req,
...sessionContext,
// Note: These two fields let us use the server-side-graphql-client library.
// We may want to remove them once the updated itemAPI w/ resolveFields is available.
executeGraphQL: rawGraphQL,
gqlNames: (listKey: string) => keystone.lists[listKey].gqlNames,
contextToReturn.graphql = {
createContext,
raw: rawGraphQL,
run: runGraphQL,
schema: graphQLSchema,
};
for (const [listKey, list] of Object.entries(keystone.lists)) {
itemAPI[listKey] = itemAPIForList(list, contextToReturn, getArgsByList[listKey]);
}
contextToReturn.executeGraphQL = rawGraphQL;

return contextToReturn;
};

const createContext = async <SessionType>({
skipAccessControl = false,
req,
res,
sessionStrategy,
}: Parameters<CreateContext<SessionType>>[0] = {}): Promise<KeystoneContext> => {
const contextToReturn = _prepareContext({ skipAccessControl, req, res, sessionStrategy });

// Build session if necessary
const session = sessionStrategy
? await sessionStrategy.get({
req: req!,
sudoContext: await createContext({
skipAccessControl: true,
req,
res,
sessionStrategy: undefined,
}),
})
: undefined;

return _bindToContext({
contextToReturn,
skipAccessControl,
req,
res,
sessionStrategy,
session,
});
};

return createContext;
}
30 changes: 10 additions & 20 deletions packages-next/keystone/src/lib/createExpressServer.ts
Expand Up @@ -6,20 +6,19 @@ import { ApolloServer } from 'apollo-server-express';
import { graphqlUploadExpress } from 'graphql-upload';
// @ts-ignore
import { formatError } from '@keystonejs/keystone/lib/Keystone/format-error';
import type { KeystoneConfig, SessionImplementation, CreateContext } from '@keystone-next/types';
import type { KeystoneConfig, SessionStrategy, CreateContext } from '@keystone-next/types';
import { createAdminUIServer } from '@keystone-next/admin-ui/system';
import { implementSession } from '../session';

const addApolloServer = ({
const addApolloServer = <SessionType>({
server,
graphQLSchema,
createContext,
sessionImplementation,
sessionStrategy,
}: {
server: express.Express;
graphQLSchema: GraphQLSchema;
createContext: CreateContext;
sessionImplementation?: SessionImplementation;
createContext: CreateContext<SessionType>;
sessionStrategy?: SessionStrategy<SessionType>;
}) => {
const apolloServer = new ApolloServer({
uploads: false,
Expand All @@ -28,10 +27,7 @@ const addApolloServer = ({
playground: { settings: { 'request.credentials': 'same-origin' } },
formatError, // TODO: this needs to be discussed
context: async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) =>
createContext({
sessionContext: await sessionImplementation?.createSessionContext(req, res, createContext),
req,
}),
createContext({ skipAccessControl: false, req, res, sessionStrategy }),
// FIXME: support for apollo studio tracing
// ...(process.env.ENGINE_API_KEY || process.env.APOLLO_KEY
// ? { tracing: true }
Expand All @@ -57,7 +53,7 @@ const addApolloServer = ({
export const createExpressServer = async (
config: KeystoneConfig,
graphQLSchema: GraphQLSchema,
createContext: CreateContext,
createContext: CreateContext<any>,
dev: boolean,
projectAdminPath: string
) => {
Expand All @@ -73,20 +69,14 @@ export const createExpressServer = async (
server.use(cors(corsConfig));
}

const sessionImplementation = config.session ? implementSession(config.session()) : undefined;
const sessionStrategy = config.session ? config.session() : undefined;

console.log('✨ Preparing GraphQL Server');
addApolloServer({ server, graphQLSchema, createContext, sessionImplementation });
addApolloServer({ server, graphQLSchema, createContext, sessionStrategy });

console.log('✨ Preparing Next.js app');
server.use(
await createAdminUIServer(
config.ui,
createContext,
dev,
projectAdminPath,
sessionImplementation
)
await createAdminUIServer(config.ui, createContext, dev, projectAdminPath, sessionStrategy)
);

return server;
Expand Down
2 changes: 1 addition & 1 deletion packages-next/keystone/src/scripts/migrate/generate.ts
Expand Up @@ -19,7 +19,7 @@ export const generate = async ({ dotKeystonePath }: StaticPaths) => {
await saveSchemaAndTypes(graphQLSchema, keystone, dotKeystonePath);

console.log('✨ Generating migration');
await keystone.connect({ context: createContext().sudo() });
await keystone.connect({ context: (await createContext()).sudo() });

await keystone.disconnect();
};
2 changes: 1 addition & 1 deletion packages-next/keystone/src/scripts/run/dev.ts
Expand Up @@ -35,7 +35,7 @@ export const dev = async ({ dotKeystonePath, projectAdminPath }: StaticPaths, sc
await saveSchemaAndTypes(graphQLSchema, keystone, dotKeystonePath);

console.log('✨ Connecting to the database');
await keystone.connect({ context: createContext().sudo() });
await keystone.connect({ context: (await createContext()).sudo() });

console.log('✨ Generating Admin UI code');
await generateAdminUI(config, graphQLSchema, keystone, projectAdminPath);
Expand Down
2 changes: 1 addition & 1 deletion packages-next/keystone/src/scripts/run/start.ts
Expand Up @@ -16,7 +16,7 @@ export const start = async ({ dotKeystonePath, projectAdminPath }: StaticPaths)
const { keystone, graphQLSchema, createContext } = createSystem(config, dotKeystonePath, 'start');

console.log('✨ Connecting to the database');
await keystone.connect({ context: createContext().sudo() });
await keystone.connect({ context: (await createContext()).sudo() });

console.log('✨ Creating server');
const server = await createExpressServer(
Expand Down

0 comments on commit b55d425

Please sign in to comment.