-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Refactor completeConfig and AuthSchema type
- Loading branch information
1 parent
06ffc4b
commit 448cb82
Showing
10 changed files
with
423 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@graphql-authz/core': patch | ||
--- | ||
|
||
Refactor completeConfig and AuthSchema type |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] } | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.