-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(astVisitor): add VisitInfo utility class which returns
node
in…
…fo and useful methods for writing middlewares BREAKING CHANGE: `astVisitor` function now requires `schemaComposer` as the second argument. `VisitKindFn` now provide just one new argument `info: VisitInfo`.
- Loading branch information
Showing
7 changed files
with
454 additions
and
98 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
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,206 @@ | ||
import { | ||
ComposeNamedOutputType, | ||
ComposeOutputType, | ||
isTypeComposer, | ||
ObjectTypeComposer, | ||
SchemaComposer, | ||
unwrapOutputTC, | ||
upperFirst, | ||
} from 'graphql-compose'; | ||
import { | ||
AstDirNode, | ||
AstFileNode, | ||
AstRootNode, | ||
AstRootTypeNode, | ||
RootTypeNames, | ||
} from './directoryToAst'; | ||
import { FieldConfig } from './typeDefs'; | ||
|
||
interface VisitInfoData<TContext = any> { | ||
node: AstDirNode | AstFileNode | AstRootTypeNode; | ||
nodeParent: AstDirNode | AstRootTypeNode | AstRootNode; | ||
operation: RootTypeNames; | ||
fieldName: string; | ||
fieldPath: string[]; | ||
schemaComposer: SchemaComposer<TContext>; | ||
} | ||
|
||
export class VisitInfo<TContext = any> { | ||
node: AstDirNode | AstFileNode | AstRootTypeNode; | ||
/** Parent AST node from directoryToAst */ | ||
nodeParent: AstDirNode | AstRootTypeNode | AstRootNode; | ||
/** Brunch of schema under which is working visitor. Can be: query, mutation, subscription */ | ||
operation: RootTypeNames; | ||
/** Name of field for current FieldConfig */ | ||
fieldName: string; | ||
/** List of parent names starting from root */ | ||
fieldPath: string[]; | ||
/** Type registry */ | ||
schemaComposer: SchemaComposer<TContext>; | ||
|
||
constructor(data: VisitInfoData<TContext>) { | ||
this.node = data.node; | ||
this.operation = data.operation; | ||
this.nodeParent = data.nodeParent; | ||
this.fieldName = data.fieldName; | ||
this.fieldPath = data.fieldPath; | ||
this.schemaComposer = data.schemaComposer; | ||
} | ||
|
||
/** | ||
* Check that this entrypoint belongs to Query | ||
*/ | ||
isQuery(): boolean { | ||
return this.operation === 'query'; | ||
} | ||
|
||
/** | ||
* Check that this entrypoint belongs to Mutation | ||
*/ | ||
isMutation(): boolean { | ||
return this.operation === 'mutation'; | ||
} | ||
|
||
/** | ||
* Check that this entrypoint belongs to Subscription | ||
*/ | ||
isSubscription(): boolean { | ||
return this.operation === 'subscription'; | ||
} | ||
|
||
/** | ||
* Return array of fieldNames. | ||
* Dotted names will be automatically splitted. | ||
* | ||
* @example | ||
* Assume: | ||
* name: 'ping' | ||
* path: ['query.storage', 'viewer', 'utils.debug'] | ||
* For empty options will be returned: | ||
* ['storage', 'viewer', 'utils', 'debug', 'ping'] | ||
* For `{ includeOperation: true }` will be returned: | ||
* ['query', 'storage', 'viewer', 'utils', 'debug', 'ping'] | ||
*/ | ||
getFieldPathArray(opts?: { includeOperation?: boolean; omitFieldName?: boolean }): string[] { | ||
const res = [] as string[]; | ||
this.fieldPath.forEach((e) => { | ||
if (e.indexOf('.')) { | ||
res.push(...e.split('.').filter(Boolean)); | ||
} else { | ||
res.push(e); | ||
} | ||
}); | ||
|
||
if (!opts?.omitFieldName) { | ||
res.push(this.fieldName); | ||
} | ||
|
||
return opts?.includeOperation ? res : res.slice(1); | ||
} | ||
|
||
/** | ||
* Return dotted path for current field | ||
*/ | ||
getFieldPathDotted(opts?: { includeOperation?: boolean; omitFieldName?: boolean }): string { | ||
return this.getFieldPathArray(opts).join('.'); | ||
} | ||
|
||
/** | ||
* Return path as CamelCase string. | ||
* | ||
* Useful for getting type name according to path | ||
*/ | ||
getFieldPathCamelCase(opts?: { includeOperation?: boolean; omitFieldName?: boolean }): string { | ||
return this.getFieldPathArray(opts) | ||
.map((s) => upperFirst(s)) | ||
.join(''); | ||
} | ||
|
||
/** | ||
* Get FieldConfig for file or dir. | ||
* This is mutable object and is shared between all calls. | ||
*/ | ||
get fieldConfig(): FieldConfig { | ||
if (this.node.kind === 'file') { | ||
return this.node.code?.default as FieldConfig; | ||
} else if (this.node.kind === 'dir' || this.node.kind === 'rootType') { | ||
return this.node.namespaceConfig?.code?.default as FieldConfig; | ||
} | ||
throw new Error( | ||
`Cannot get fieldConfig. Node has some strange kind: ${(this.node as any).kind}` | ||
); | ||
} | ||
|
||
/** | ||
* Get TypeComposer instance for output type (object, scalar, enum, interface, union). | ||
* It's mutable object. | ||
*/ | ||
getOutputAnyTC(): ComposeOutputType<TContext> { | ||
const fc = this.fieldConfig; | ||
const outputType = fc.type; | ||
if (!outputType) { | ||
throw new Error(`FieldConfig ${this.getFieldPathDotted()} does not have 'type' property`); | ||
} | ||
|
||
// if the type is of any kind of TypeComposer | ||
// then return it directly | ||
// or try to convert it to TypeComposer and save in FieldConfig as prepared type | ||
if (isTypeComposer(outputType)) { | ||
return outputType; | ||
} else { | ||
const outputTC = this.schemaComposer.typeMapper.convertOutputTypeDefinition( | ||
outputType, | ||
this.fieldName, | ||
this.nodeParent?.name | ||
); | ||
|
||
if (!outputTC) { | ||
throw new Error( | ||
`FieldConfig ${this.getFieldPathDotted()} contains some strange value as output type` | ||
); | ||
} | ||
|
||
fc.type = outputTC; | ||
return outputTC; | ||
} | ||
} | ||
|
||
/** | ||
* Check that output type is an object | ||
*/ | ||
isOutputTypeIsObject(): boolean { | ||
return this.getOutputAnyTC() instanceof ObjectTypeComposer; | ||
} | ||
|
||
/** | ||
* Get TypeComposer instance for output type (object, scalar, enum, interface, union). | ||
* It's mutable object. | ||
*/ | ||
getOutputUnwrappedTC(): ComposeNamedOutputType<TContext> { | ||
return unwrapOutputTC(this.getOutputAnyTC()); | ||
} | ||
|
||
/** | ||
* Get TypeComposer instance for output type (object, scalar, enum, interface, union). | ||
* It's mutable object. | ||
*/ | ||
getOutputUnwrappedOTC(): ObjectTypeComposer { | ||
const tc = unwrapOutputTC(this.getOutputAnyTC()); | ||
|
||
if (!(tc instanceof ObjectTypeComposer)) { | ||
throw new Error( | ||
`FieldConfig ${this.getFieldPathDotted()} has non-Object output type. Use 'isOutputTypeIsObject()' before for avoiding this error.` | ||
); | ||
} | ||
|
||
return tc; | ||
} | ||
|
||
toString(): string { | ||
return `VisitInfo(${this.getFieldPathDotted({ includeOperation: true })})`; | ||
} | ||
|
||
toJSON(): string { | ||
return this.toString(); | ||
} | ||
} |
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,161 @@ | ||
import { | ||
ListComposer, | ||
ObjectTypeComposer, | ||
ScalarTypeComposer, | ||
SchemaComposer, | ||
} from 'graphql-compose'; | ||
import { AstFileNode, AstRootNode, VisitInfo } from '..'; | ||
|
||
const schemaComposer = new SchemaComposer(); | ||
const nodeParent = { | ||
absPath: 'schema/query', | ||
children: {}, | ||
kind: 'root', | ||
name: 'query', | ||
} as AstRootNode; | ||
const node = { | ||
absPath: 'schema/query/some_endpoint.ts', | ||
code: { | ||
default: { | ||
type: 'String', | ||
resolve: () => 'Hello!', | ||
}, | ||
}, | ||
kind: 'file', | ||
name: 'some_endpoint', | ||
} as AstFileNode; | ||
|
||
beforeEach(() => { | ||
schemaComposer.clear(); | ||
}); | ||
|
||
describe('VisitInfo', () => { | ||
it('getFieldPathArray()', () => { | ||
const info = new VisitInfo({ | ||
operation: 'query', | ||
fieldName: 'ping', | ||
fieldPath: ['query.storage', 'viewer', 'utils.debug'], | ||
node, | ||
nodeParent, | ||
schemaComposer, | ||
}); | ||
|
||
expect(info.getFieldPathArray()).toEqual(['storage', 'viewer', 'utils', 'debug', 'ping']); | ||
expect(info.getFieldPathArray({ omitFieldName: true })).toEqual([ | ||
'storage', | ||
'viewer', | ||
'utils', | ||
'debug', | ||
]); | ||
expect(info.getFieldPathArray({ includeOperation: true })).toEqual([ | ||
'query', | ||
'storage', | ||
'viewer', | ||
'utils', | ||
'debug', | ||
'ping', | ||
]); | ||
}); | ||
|
||
it('getFieldPathDotted()', () => { | ||
const info = new VisitInfo({ | ||
operation: 'query', | ||
fieldName: 'ping', | ||
fieldPath: ['query.storage', 'viewer', 'utils.debug'], | ||
node, | ||
nodeParent, | ||
schemaComposer, | ||
}); | ||
|
||
expect(info.getFieldPathDotted()).toEqual('storage.viewer.utils.debug.ping'); | ||
expect(info.getFieldPathDotted({ omitFieldName: true })).toEqual('storage.viewer.utils.debug'); | ||
expect(info.getFieldPathDotted({ includeOperation: true })).toEqual( | ||
'query.storage.viewer.utils.debug.ping' | ||
); | ||
}); | ||
|
||
it('getFieldPathCamelCase()', () => { | ||
const info = new VisitInfo({ | ||
operation: 'query', | ||
fieldName: 'ping', | ||
fieldPath: ['query.storage', 'viewer', 'utils.debug'], | ||
node, | ||
nodeParent, | ||
schemaComposer, | ||
}); | ||
|
||
expect(info.getFieldPathCamelCase()).toEqual('StorageViewerUtilsDebugPing'); | ||
expect(info.getFieldPathCamelCase({ omitFieldName: true })).toEqual('StorageViewerUtilsDebug'); | ||
expect(info.getFieldPathCamelCase({ includeOperation: true })).toEqual( | ||
'QueryStorageViewerUtilsDebugPing' | ||
); | ||
}); | ||
|
||
it('get fieldConfig', () => { | ||
const info = new VisitInfo({ | ||
operation: 'query', | ||
fieldName: 'ping', | ||
fieldPath: ['query.storage', 'viewer', 'utils.debug'], | ||
node, | ||
nodeParent, | ||
schemaComposer, | ||
}); | ||
|
||
const { fieldConfig } = info; | ||
expect(fieldConfig).toEqual({ resolve: expect.anything(), type: 'String' }); | ||
}); | ||
|
||
describe('methods for output type', () => { | ||
const info = new VisitInfo({ | ||
operation: 'query', | ||
fieldName: 'ping', | ||
fieldPath: ['query.storage', 'viewer', 'utils.debug'], | ||
node, | ||
nodeParent, | ||
schemaComposer, | ||
}); | ||
|
||
it('getOutputAnyTC() with Scalar', () => { | ||
const tc = info.getOutputAnyTC(); | ||
expect(tc instanceof ScalarTypeComposer).toBeTruthy(); | ||
expect(tc.getTypeName()).toEqual('String'); | ||
}); | ||
|
||
it('getOutputAnyTC() with List', () => { | ||
info.fieldConfig.type = '[String!]'; | ||
const tc = info.getOutputAnyTC(); | ||
expect(tc instanceof ListComposer).toBeTruthy(); | ||
expect(tc.getTypeName()).toEqual('[String!]'); | ||
}); | ||
|
||
it('isOutputTypeIsObject()', () => { | ||
info.fieldConfig.type = 'String'; | ||
expect(info.isOutputTypeIsObject()).toBeFalsy(); | ||
info.fieldConfig.type = '[String!]'; | ||
expect(info.isOutputTypeIsObject()).toBeFalsy(); | ||
info.fieldConfig.type = 'type MyObj { a: Int }'; | ||
expect(info.isOutputTypeIsObject()).toBeTruthy(); | ||
}); | ||
|
||
it('getOutputUnwrappedTC()', () => { | ||
info.fieldConfig.type = 'String'; | ||
expect(info.getOutputUnwrappedTC() instanceof ScalarTypeComposer).toBeTruthy(); | ||
expect(info.getOutputUnwrappedTC().getTypeName()).toBe('String'); | ||
info.fieldConfig.type = '[String!]'; | ||
expect(info.getOutputUnwrappedTC() instanceof ScalarTypeComposer).toBeTruthy(); | ||
expect(info.getOutputUnwrappedTC().getTypeName()).toBe('String'); | ||
info.fieldConfig.type = ['type MyObj { a: Int }']; | ||
expect(info.getOutputUnwrappedTC() instanceof ObjectTypeComposer).toBeTruthy(); | ||
expect(info.getOutputUnwrappedTC().getTypeName()).toBe('MyObj'); | ||
}); | ||
|
||
it('getOutputUnwrappedTC()', () => { | ||
info.fieldConfig.type = 'String'; | ||
expect(() => info.getOutputUnwrappedOTC()).toThrowError(/has non-Object output type/); | ||
|
||
info.fieldConfig.type = ['type MyObj { a: Int }']; | ||
expect(info.getOutputUnwrappedOTC() instanceof ObjectTypeComposer).toBeTruthy(); | ||
expect(info.getOutputUnwrappedOTC().getTypeName()).toBe('MyObj'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.