Skip to content

Commit

Permalink
chore: Refactor completeConfig and AuthSchema type
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidTkachenkoAstrumu committed Jul 29, 2024
1 parent 06ffc4b commit 448cb82
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 138 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-needles-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-authz/core': patch
---

Refactor completeConfig and AuthSchema type
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@
"prettier": "2.3.2",
"rimraf": "3.0.2",
"ts-jest": "27.0.7",
"typescript": "4.9.3"
"typescript": "5.5.4"
}
}
119 changes: 119 additions & 0 deletions packages/core/src/__tests__/auth-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { InternalAuthSchema } from '../auth-schema';

describe('InternalAuthSchema', () => {
it('extracts configuration for a type from correct schema if it is present', () => {
const authSchema = new InternalAuthSchema('__authz', {
'*': { __authz: { rules: ['Wildcard'] } },
Query: { __authz: { rules: ['Query'] } },
Mutation: {
'*': { __authz: { rules: ['Mutation wildcard'] } }
},
User: { __authz: { rules: ['User'] } },
Posts: {
author: { __authz: { rules: ['Posts author'] } }
}
});

expect(authSchema.getTypeRuleConfig('*')).toStrictEqual({
rules: ['Wildcard']
});
expect(authSchema.getTypeRuleConfig('Query')).toStrictEqual({
rules: ['Query']
});
expect(authSchema.getTypeRuleConfig('User')).toStrictEqual({
rules: ['User']
});

expect(authSchema.getTypeRuleConfig('Mutation')).toBeUndefined();
expect(authSchema.getTypeRuleConfig('Posts')).toBeUndefined();
});

it('extracts configuration for a type-field pair from correct schema if it is present', () => {
const authSchema = new InternalAuthSchema('__authz', {
'*': {
author: { __authz: { rules: ['Wildcard author'] } }
},
Query: { __authz: { rules: ['Query'] } },
Mutation: {
'*': { __authz: { rules: ['Mutation wildcard'] } }
},
User: { __authz: { rules: ['User'] } },
Posts: {
author: { __authz: { rules: ['Posts author'] } }
}
});

expect(authSchema.getFieldRuleConfig('*', 'author')).toStrictEqual({
rules: ['Wildcard author']
});
expect(authSchema.getFieldRuleConfig('*', 'reader')).toBeUndefined();

expect(authSchema.getFieldRuleConfig('Query', '*')).toBeUndefined();
expect(authSchema.getFieldRuleConfig('Query', 'author')).toBeUndefined();

expect(authSchema.getFieldRuleConfig('Mutation', '*')).toStrictEqual({
rules: ['Mutation wildcard']
});
expect(authSchema.getFieldRuleConfig('Mutation', 'author')).toBeUndefined();

expect(authSchema.getFieldRuleConfig('User', '*')).toBeUndefined();
expect(authSchema.getFieldRuleConfig('User', 'reader')).toBeUndefined();

expect(authSchema.getFieldRuleConfig('Posts', '*')).toBeUndefined();
expect(authSchema.getFieldRuleConfig('Posts', 'author')).toStrictEqual({
rules: ['Posts author']
});
});

it('extracts ALL declared configurations provided in schema', () => {
const authSchema = new InternalAuthSchema('__authz', {
'*': {
author: { __authz: { rules: ['Wildcard author'] } }
},
Query: { __authz: { rules: ['Query'] } },
Mutation: {
'*': { __authz: { rules: ['Mutation wildcard'] } }
},
User: { __authz: { rules: ['User'] } },
Posts: {
author: { __authz: { rules: ['Posts author'] } }
}
});

expect(authSchema.getAllRuleConfigs()).toStrictEqual([
{ rules: ['Wildcard author'] },
{ rules: ['Query'] },
{ rules: ['Mutation wildcard'] },
{ rules: ['User'] },
{ rules: ['Posts author'] }
]);
});

it('cannot extract elements from schema if wrapped in a wrong custom schema key', () => {
const authSchema = new InternalAuthSchema('correctKey', {
'*': {
author: { correctKey: { rules: ['Wildcard author'] } }
},
Query: { correctKey: { rules: ['Query'] } },
Mutation: {
'*': { wrongKey: { rules: ['Mutation wildcard'] } }
},
User: { wrongKey: { rules: ['User'] } }
});

expect(authSchema.getTypeRuleConfig('User')).toBeUndefined();
expect(authSchema.getFieldRuleConfig('Mutation', '*')).toBeUndefined();

expect(authSchema.getTypeRuleConfig('Query')).toStrictEqual({
rules: ['Query']
});
expect(authSchema.getFieldRuleConfig('*', 'author')).toStrictEqual({
rules: ['Wildcard author']
});

expect(authSchema.getAllRuleConfigs()).toStrictEqual([
{ rules: ['Wildcard author'] },
{ rules: ['Query'] }
]);
});
});
38 changes: 38 additions & 0 deletions packages/core/src/__tests__/configs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RulesObject, InstantiableRule } from '../rules';
import { completeConfig } from '../config';

describe('completeConfig', () => {
it('Throws error if auth schema contains not registered rules', () => {
const authSchema = {
'*': { __authz: { rules: ['Registered rule 1'] } },
Query: { __authz: { rules: ['Registered rule 2'] } },
Mutation: {
'*': { __authz: { rules: ['Not registered rule 2'] } }
},
User: { __authz: { rules: ['Not registered rule 2'] } }
};
const rulesRegistry: RulesObject = {
'Registered rule 1': {} as InstantiableRule,
'Registered rule 2': {} as InstantiableRule
};

expect(() =>
completeConfig({ rules: rulesRegistry, authSchema })
).toThrowError();
});

it('Do nothing if all rules are registered', () => {
const authSchema = {
'*': { __authz: { rules: ['Registered rule 1'] } },
Query: { __authz: { rules: ['Registered rule 2'] } }
};
const rulesRegistry: RulesObject = {
'Registered rule 1': {} as InstantiableRule,
'Registered rule 2': {} as InstantiableRule
};

expect(() =>
completeConfig({ rules: rulesRegistry, authSchema })
).not.toThrowError();
});
});
45 changes: 45 additions & 0 deletions packages/core/src/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isDefined, isNil } from '../helpers';

describe('isDefined', () => {
it('returns true if value is present', () => {
expect(isDefined(new Date())).toBeTruthy();
expect(isDefined('')).toBeTruthy();
expect(isDefined('string')).toBeTruthy();
expect(isDefined(1)).toBeTruthy();
expect(isDefined(0)).toBeTruthy();
expect(isDefined(true)).toBeTruthy();
expect(isDefined(false)).toBeTruthy();
expect(isDefined([])).toBeTruthy();
expect(isDefined([''])).toBeTruthy();
expect(isDefined([0, 1])).toBeTruthy();
expect(isDefined({})).toBeTruthy();
expect(isDefined({ any: '' })).toBeTruthy();
});

it('returns false for undefined and null', () => {
expect(isDefined(undefined)).toBeFalsy();
expect(isDefined(null)).toBeFalsy();
});
});

describe('isNil', () => {
it('returns true if value is present', () => {
expect(isNil(new Date())).toBeFalsy();
expect(isNil('')).toBeFalsy();
expect(isNil('string')).toBeFalsy();
expect(isNil(1)).toBeFalsy();
expect(isNil(0)).toBeFalsy();
expect(isNil(true)).toBeFalsy();
expect(isNil(false)).toBeFalsy();
expect(isNil([])).toBeFalsy();
expect(isNil([''])).toBeFalsy();
expect(isNil([0, 1])).toBeFalsy();
expect(isNil({})).toBeFalsy();
expect(isNil({ any: '' })).toBeFalsy();
});

it('returns true for undefined and null', () => {
expect(isNil(undefined)).toBeTruthy();
expect(isNil(null)).toBeTruthy();
});
});
99 changes: 95 additions & 4 deletions packages/core/src/auth-schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,97 @@
import { IAuthConfig } from './auth-config';
import { isDefined, isNil, ToDeepDictionary } from './helpers';

export type AuthSchema = Record<
string,
Record<string, Record<string, IAuthConfig> | IAuthConfig>
>;
/**
* GraphQL-style definition of your validation rules applied to
* [objects types and fields](https://graphql.org/learn/schema/#object-types-and-fields) in your GraphQL schema.
* [See our authz-schema docs section for some usage examples](https://github.com/AstrumU/graphql-authz?tab=readme-ov-file#using-authschema).
*
* For both `typeName` and `fieldName` definition [you could use a wildcard (`*`)](https://github.com/AstrumU/graphql-authz?tab=readme-ov-file#wildcard-rules)
* to apply your rule for all types or fields within a type.
*
* @param typeName - represents your GraphQL type object. These are your custom types and common [Query, Mutation](https://graphql.org/learn/schema/#the-query-and-mutation-types).
* @param fieldName - represents your GraphQL fields values.
* @param __authz - is your custom defined [introspection field name](https://graphql.org/learn/introspection/), which stores all your Authz rules definitions. By default it is `__authz` string.
*/
export type AuthSchema = {
[typeName: string | '*']:
| {
[fieldName: string | '*']: {
[__authz: string]: IAuthConfig;
};
}
| {
[__authz: string]: IAuthConfig;
};
};

type TypeSchema = ToDeepDictionary<{
[fieldName: string | '*']: {
[__authz: string]: IAuthConfig;
};
}>
type RuleConfig = ToDeepDictionary<{
[__authz: string]: IAuthConfig;
}>;
type TypeSchemaOrRuleConfig = TypeSchema | RuleConfig;

/**
* Representation of authentication rules configuration. @see AuthSchema
*/
export class InternalAuthSchema {
private rulesSchemaKey: string;

private schema: ToDeepDictionary<AuthSchema>;

constructor(rulesSchemaKey: string, schema: AuthSchema) {
this.rulesSchemaKey = rulesSchemaKey
this.schema = schema
}

public static createIfCan(rulesSchemaKey: string, schema: AuthSchema | undefined): InternalAuthSchema | undefined {
return isDefined(schema) ? new InternalAuthSchema(rulesSchemaKey, schema) : undefined;
}

public getTypeRuleConfig(typeName: string): IAuthConfig | undefined {
const typeDeclaration = this.schema[typeName];
return typeDeclaration?.[this.rulesSchemaKey];
}

private getTypeAuthSchema(typeName: string): TypeSchema | undefined {
const typeDeclaration = this.schema[typeName];
if (isNil(typeDeclaration) || this.isRuleConfig(typeDeclaration)) {
return undefined
}

return typeDeclaration;
}

private isRuleConfig(typeDeclaration: TypeSchemaOrRuleConfig): typeDeclaration is RuleConfig {
return this.rulesSchemaKey in typeDeclaration;
}

public getFieldRuleConfig(
typeName: string,
fieldName: string,
): IAuthConfig | undefined {
const typeSchema = this.getTypeAuthSchema(typeName);
return typeSchema?.[fieldName]?.[this.rulesSchemaKey];
}

public getAllRuleConfigs(): IAuthConfig[] {
return Object.keys(this.schema)
.flatMap(typeName => this.getAllTypeRules(typeName))
}

private getAllTypeRules = (typeName: string): IAuthConfig[] => {
const fullTypeRuleConfig = this.getTypeRuleConfig(typeName);
if (isDefined(fullTypeRuleConfig)) {
return [fullTypeRuleConfig]
}

const typeSchema = this.getTypeAuthSchema(typeName);
return Object.keys(typeSchema ?? {})
.map(fieldName => this.getFieldRuleConfig(typeName, fieldName))
.filter(isDefined)
}
}
Loading

0 comments on commit 448cb82

Please sign in to comment.