Skip to content

Commit

Permalink
Merge pull request #1189 from awinograd/main
Browse files Browse the repository at this point in the history
[auth] Add RequestCache.clearForContext and export RequestCache class
  • Loading branch information
hayes committed Apr 17, 2024
2 parents c67d1c8 + ae62a17 commit 24d070f
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-numbers-jog.md
@@ -0,0 +1,5 @@
---
'@pothos/plugin-scope-auth': minor
---

[auth] Allow clearing/resetting scope cache in the middle of a request
6 changes: 6 additions & 0 deletions .changeset/rotten-clocks-remember.md
@@ -0,0 +1,6 @@
---
'@pothos/plugin-prisma': patch
'@pothos/core': patch
---

Add delete method to context caches
18 changes: 13 additions & 5 deletions packages/core/src/utils/context-cache.ts
Expand Up @@ -6,17 +6,17 @@ export function initContextCache() {
};
}

export type ContextCache<T, C extends object, Args extends unknown[]> = (
context: C,
...args: Args
) => T;
export interface ContextCache<T, C extends object, Args extends unknown[]> {
(context: C, ...args: Args): T;
delete: (context: C) => void;
}

export function createContextCache<T, C extends object = object, Args extends unknown[] = []>(
create: (context: C, ...args: Args) => T,
): ContextCache<T, C, Args> {
const cache = new WeakMap<object, T>();

return (context, ...args) => {
const getOrCreate = (context: C, ...args: Args) => {
const cacheKey = (context as { [contextCacheSymbol]: object })[contextCacheSymbol] || context;

if (cache.has(cacheKey)) {
Expand All @@ -29,4 +29,12 @@ export function createContextCache<T, C extends object = object, Args extends un

return entry;
};

getOrCreate.delete = (context: C) => {
const cacheKey = (context as { [contextCacheSymbol]: object })[contextCacheSymbol] || context;

cache.delete(cacheKey);
};

return getOrCreate;
}
6 changes: 4 additions & 2 deletions packages/plugin-prisma/src/util/get-client.ts
Expand Up @@ -42,7 +42,7 @@ export interface RuntimeDataModel {
}

const prismaClientCache = createContextCache(
<Types extends SchemaTypes>(builder: PothosSchemaTypes.SchemaBuilder<Types>) =>
(builder: PothosSchemaTypes.SchemaBuilder<SchemaTypes>) =>
createContextCache((context: object) =>
typeof builder.options.prisma.client === 'function'
? builder.options.prisma.client(context)
Expand All @@ -55,7 +55,9 @@ export function getClient<Types extends SchemaTypes>(
context: Types['Context'],
): PrismaClient {
if (typeof builder.options.prisma.client === 'function') {
return prismaClientCache(builder)(context);
return prismaClientCache(builder as unknown as PothosSchemaTypes.SchemaBuilder<SchemaTypes>)(
context,
);
}

return builder.options.prisma.client;
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-scope-auth/src/index.ts
Expand Up @@ -17,6 +17,7 @@ import SchemaBuilder, {
SchemaTypes,
} from '@pothos/core';
import { isTypeOfHelper } from './is-type-of-helper';
import RequestCache from './request-cache';
import { resolveHelper } from './resolve-helper';
import {
createFieldAuthScopesStep,
Expand All @@ -27,6 +28,7 @@ import {
} from './steps';
import { ResolveStep, TypeAuthScopes, TypeGrantScopes } from './types';

export { RequestCache };
export * from './errors';
export * from './types';

Expand Down
23 changes: 15 additions & 8 deletions packages/plugin-scope-auth/src/request-cache.ts
@@ -1,6 +1,13 @@
/* eslint-disable @typescript-eslint/promise-function-async */
import { GraphQLResolveInfo } from 'graphql';
import { isThenable, MaybePromise, Path, PothosValidationError, SchemaTypes } from '@pothos/core';
import {
createContextCache,
isThenable,
MaybePromise,
Path,
PothosValidationError,
SchemaTypes,
} from '@pothos/core';
import {
AuthFailure,
AuthScopeFailureType,
Expand All @@ -10,8 +17,9 @@ import {
} from './types';
import { cacheKey, canCache } from './util';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const requestCache = new WeakMap<{}, RequestCache<any>>();
const contextCache = createContextCache(
(ctx, builder: PothosSchemaTypes.SchemaBuilder<SchemaTypes>) => new RequestCache(builder, ctx),
);

export default class RequestCache<Types extends SchemaTypes> {
builder;
Expand Down Expand Up @@ -49,12 +57,11 @@ export default class RequestCache<Types extends SchemaTypes> {
context: T['Context'],
builder: PothosSchemaTypes.SchemaBuilder<T>,
): RequestCache<T> {
if (!requestCache.has(context)) {
requestCache.set(context, new RequestCache<T>(builder, context));
}
return contextCache(context, builder as never) as never;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return requestCache.get(context)!;
static clearForContext<T extends SchemaTypes>(context: T['Context']): void {
contextCache.delete(context);
}

getScopes(): MaybePromise<ScopeLoaderMap<Types>> {
Expand Down
Expand Up @@ -111,6 +111,7 @@ type Post {
}
type Query {
ClearCache: ObjForSyncPermFn
IfaceBooleanFn(result: Boolean!): IfaceBooleanFn
IfaceForAdmin: IfaceForAdmin
ObjAdminIface: ObjAdminIface
Expand Down
36 changes: 36 additions & 0 deletions packages/plugin-scope-auth/tests/caching.test.ts
Expand Up @@ -226,4 +226,40 @@ describe('caching', () => {
`);
});
});

it('clears cache during request', async () => {
const query = gql`
query {
obj: ClearCache {
field
}
}
`;

const counter = new Counter();

const result = await execute({
schema,
document: query,
contextValue: {
count: counter.count,
user: new User({
'x-user-id': '1',
'x-permissions': 'a',
}),
},
});

expect(counter.counts.get('authScopes')).toBe(2);

expect(result).toMatchInlineSnapshot(`
{
"data": {
"obj": {
"field": "ok",
},
},
}
`);
});
});
32 changes: 20 additions & 12 deletions packages/plugin-scope-auth/tests/example/builder.ts
Expand Up @@ -42,20 +42,28 @@ const builder = new SchemaBuilder<{
authorizeOnSubscribe: true,
defaultStrategy: 'all',
},
authScopes: async (context) => ({
loggedIn: !!context.user,
admin: !!context.user?.roles.includes('admin'),
syncPermission: (perm) => {
context.count?.('syncPermission');
authScopes: async (context) => {
context.count?.('authScopes');

return !!context.user?.permissions.includes(perm);
},
asyncPermission: async (perm) => {
context.count?.('asyncPermission');
// locally reference use to simulate data loaded in this authScopes fn that depends on incoming
// context data and is not modifiable from resolvers
const { user } = context;

return !!context.user?.permissions.includes(perm);
},
}),
return {
loggedIn: !!user,
admin: !!user?.roles.includes('admin'),
syncPermission: (perm) => {
context.count?.('syncPermission');

return !!user?.permissions.includes(perm);
},
asyncPermission: async (perm) => {
context.count?.('asyncPermission');

return !!user?.permissions.includes(perm);
},
};
},
});

export default builder;
19 changes: 19 additions & 0 deletions packages/plugin-scope-auth/tests/example/schema/index.ts
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/require-await */
import './custom-errors';
import './with-auth';
import { RequestCache } from '../../../src';
import builder from '../builder';
import User from '../user';

builder.queryField('currentId', (t) =>
t.authField({
Expand Down Expand Up @@ -847,6 +849,23 @@ builder.queryType({

resolve: () => ({}),
}),
ClearCache: t.field({
type: ObjForSyncPermFn,
nullable: true,
authScopes: {
syncPermission: 'a',
},
resolve: (parent, args, context) => {
context.user = new User({
'x-user-id': '1',
'x-permissions': 'b',
});

RequestCache.clearForContext(context);

return { permission: 'b' };
},
}),
}),
});

Expand Down

0 comments on commit 24d070f

Please sign in to comment.