Skip to content

Commit

Permalink
[auth] Add RequestCache.clearForContext and export RequestCache class
Browse files Browse the repository at this point in the history
If the scope initializer loads data that may change during a request
for example due to a new user session or permission change then we need
a way to clear the cache such that the scope initializer can be
re-executed based on the new context state.
  • Loading branch information
awinograd committed Apr 16, 2024
1 parent c67d1c8 commit c78380d
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 12 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
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
4 changes: 4 additions & 0 deletions packages/plugin-scope-auth/src/request-cache.ts
Expand Up @@ -57,6 +57,10 @@ export default class RequestCache<Types extends SchemaTypes> {
return requestCache.get(context)!;
}

static clearForContext<T extends SchemaTypes>(context: T['Context']): void {
requestCache.delete(context);
}

getScopes(): MaybePromise<ScopeLoaderMap<Types>> {
if (!this.scopes) {
const scopes = this.builder.options.authScopes(this.context);
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 c78380d

Please sign in to comment.