Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Omit @nonExposed fields from nested GraphQL filter and sort inputs.",
"type": "patch",
"packageName": "@magek/core"
}
],
"packageName": "@magek/core",
"email": "omidomirzaei@gmail.com"
}
18 changes: 16 additions & 2 deletions packages/core/src/decorators/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnyClass, ClassMetadata } from '@magek/common'
import { AnyClass, ClassMetadata, MagekConfig } from '@magek/common'
import { buildClassMetadataFromFields } from './field-metadata-reader'
import { DecoratorMetadataObject } from './decorator-types'
import { DecoratorMetadataObject, SYMBOL_METADATA } from './decorator-types'
import { NON_EXPOSED_SYMBOL } from './non-exposed'

/**
Expand All @@ -13,6 +13,20 @@ export function getNonExposedFields(metadata?: DecoratorMetadataObject): string[
return (metadata?.[NON_EXPOSED_SYMBOL] as string[]) || []
}

/**
* Fields that must be left out of GraphQL for a class.
* Class decorators register the list under the class name. A nested class with no
* class decorator only has the names on its own decorator metadata.
*/
export function nonExposedFieldsForClass(config: MagekConfig, type: AnyClass): string[] {
const registered = config.nonExposedGraphQLMetadataKey[type.name]
if (registered) {
return registered
}
const classRecord = type as unknown as Record<symbol, DecoratorMetadataObject | undefined>
return getNonExposedFields(classRecord[SYMBOL_METADATA])
}

/**
* Get class metadata from @field() decorators.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
GraphQLString,
ThunkObjMap,
} from 'graphql'
import { getClassMetadata } from '../../../decorators/metadata'
import { getClassMetadata, nonExposedFieldsForClass } from '../../../decorators/metadata'
import { PropertyMetadata, TypeMetadata } from '@magek/common'
import { GraphQLJSON } from 'graphql-scalars'
import { AnyClass, MagekConfig, UUID } from '@magek/common'
Expand Down Expand Up @@ -54,11 +54,13 @@ export class GraphqlQueryFilterArgumentsBuilder {
let nestedProperties: ThunkObjMap<GraphQLInputFieldConfig> = {}
const metadata = getClassMetadata(prop.typeInfo.type)
if (metadata.fields.length === 0) return GraphQLJSON
const excludeProps = this.config.nonExposedGraphQLMetadataKey[prop.name]
this.typeInformer.generateGraphQLTypeForClass(prop.typeInfo.type, excludeProps, true)
const excludeProps = nonExposedFieldsForClass(this.config, prop.typeInfo.type)
// Do not pass exclusions into the shared input-type cache. Queries are built
// before mutations, and a cached AddressInput would then drop the field there too.
this.typeInformer.generateGraphQLTypeForClass(prop.typeInfo.type, [], true)

for (const prop of metadata.fields) {
const property = { [prop.name]: { type: this.generateFilterFor(prop) } }
for (const nestedProp of nonExcludedFields(metadata.fields, excludeProps)) {
const property = { [nestedProp.name]: { type: this.generateFilterFor(nestedProp) } }
nestedProperties = { ...nestedProperties, ...property }
}
fields = () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GraphQLEnumType, GraphQLFieldConfigArgumentMap, GraphQLInputObjectType, ThunkObjMap } from 'graphql'
import { PropertyMetadata } from '@magek/common'
import { getClassMetadata } from '../../../decorators/metadata'
import { getClassMetadata, nonExposedFieldsForClass } from '../../../decorators/metadata'
import { buildGraphqlSimpleEnumFor, isExternalType, nonExcludedFields } from '../common'
import { GraphQLInputFieldConfig } from 'graphql/type/definition'
import { GraphQLTypeInformer } from '../graphql-type-informer'
Expand Down Expand Up @@ -42,11 +42,13 @@ export class GraphqlQuerySortBuilder {
let nestedProperties: ThunkObjMap<GraphQLInputFieldConfig> = {}
const metadata = getClassMetadata(prop.typeInfo.type)
if (metadata.fields.length === 0) return this.orderType
const excludeProps = this.config.nonExposedGraphQLMetadataKey[prop.name]
this.typeInformer.generateGraphQLTypeForClass(prop.typeInfo.type, excludeProps, true)
const excludeProps = nonExposedFieldsForClass(this.config, prop.typeInfo.type)
// Do not pass exclusions into the shared input-type cache. Queries are built
// before mutations, and a cached AddressInput would then drop the field there too.
this.typeInformer.generateGraphQLTypeForClass(prop.typeInfo.type, [], true)

for (const prop of metadata.fields) {
const property = { [prop.name]: { type: this.generateSortFor(prop) } }
for (const nestedProp of nonExcludedFields(metadata.fields, excludeProps)) {
const property = { [nestedProp.name]: { type: this.generateSortFor(nestedProp) } }
nestedProperties = { ...nestedProperties, ...property }
}
fields = () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect } from '../../../expect'
import { GraphQLInputObjectType } from 'graphql'
import { MagekConfig, getLogger } from '@magek/common'
import { field, nonExposed } from '../../../../src/decorators'
import { GraphQLTypeInformer } from '../../../../src/services/graphql/graphql-type-informer'
import { GraphqlQueryFilterArgumentsBuilder } from '../../../../src/services/graphql/query-helpers/graphql-query-filter-arguments-builder'

function filterFieldsFor(
parent: new (...args: unknown[]) => unknown,
nested: new (...args: unknown[]) => unknown,
config: MagekConfig
): { filterFields: ReturnType<GraphQLInputObjectType['getFields']>; inputType: GraphQLInputObjectType } {
const logger = getLogger(config, 'test')
const typeInformer = new GraphQLTypeInformer(logger)
const builder = new GraphqlQueryFilterArgumentsBuilder(typeInformer, {}, config)
const args = builder.generateFilterArguments(parent, [])
const filterType = args.address.type as GraphQLInputObjectType
const inputType = typeInformer.generateGraphQLTypeForClass(nested, ['internalCode'], true) as GraphQLInputObjectType
return { filterFields: filterType.getFields(), inputType }
}

describe('GraphqlQueryFilterArgumentsBuilder nested @nonExposed fields', () => {
it('omits @nonExposed fields on a nested class that is not itself registered', () => {
class MetadataAddress {
@field((type) => String)
public readonly street!: string

@nonExposed
@field((type) => String)
public readonly internalCode!: string
}

class MetadataUser {
@field((type) => MetadataAddress)
public readonly address!: MetadataAddress
}

const { filterFields, inputType } = filterFieldsFor(MetadataUser, MetadataAddress, new MagekConfig('test'))

expect(filterFields).to.have.property('street')
expect(filterFields).to.not.have.property('internalCode')
expect(inputType.getFields()).to.have.property('internalCode')
})

it('omits fields listed for the nested class name in nonExposedGraphQLMetadataKey', () => {
class ConfigAddress {
@field((type) => String)
public readonly street!: string

@field((type) => String)
public readonly internalCode!: string
}

class ConfigUser {
@field((type) => ConfigAddress)
public readonly address!: ConfigAddress
}

const config = new MagekConfig('test')
config.nonExposedGraphQLMetadataKey[ConfigAddress.name] = ['internalCode']

const { filterFields } = filterFieldsFor(ConfigUser, ConfigAddress, config)

expect(filterFields).to.have.property('street')
expect(filterFields).to.not.have.property('internalCode')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect } from '../../../expect'
import { GraphQLInputObjectType } from 'graphql'
import { MagekConfig, getLogger } from '@magek/common'
import { field, nonExposed } from '../../../../src/decorators'
import { GraphQLTypeInformer } from '../../../../src/services/graphql/graphql-type-informer'
import { GraphqlQuerySortBuilder } from '../../../../src/services/graphql/query-helpers/graphql-query-sort-builder'

function sortFieldsFor(
parent: new (...args: unknown[]) => unknown,
nested: new (...args: unknown[]) => unknown,
config: MagekConfig
): { sortFields: ReturnType<GraphQLInputObjectType['getFields']>; inputType: GraphQLInputObjectType } {
const logger = getLogger(config, 'test')
const typeInformer = new GraphQLTypeInformer(logger)
const builder = new GraphqlQuerySortBuilder(typeInformer, config)
const args = builder.generateSortArguments(parent, [])
const sortType = args.address.type as GraphQLInputObjectType
const inputType = typeInformer.generateGraphQLTypeForClass(nested, ['internalCode'], true) as GraphQLInputObjectType
return { sortFields: sortType.getFields(), inputType }
}

describe('GraphqlQuerySortBuilder nested @nonExposed fields', () => {
it('omits @nonExposed fields on a nested class that is not itself registered', () => {
class MetadataAddress {
@field((type) => String)
public readonly street!: string

@nonExposed
@field((type) => String)
public readonly internalCode!: string
}

class MetadataUser {
@field((type) => MetadataAddress)
public readonly address!: MetadataAddress
}

const { sortFields, inputType } = sortFieldsFor(MetadataUser, MetadataAddress, new MagekConfig('test'))

expect(sortFields).to.have.property('street')
expect(sortFields).to.not.have.property('internalCode')
expect(inputType.getFields()).to.have.property('internalCode')
})

it('omits fields listed for the nested class name in nonExposedGraphQLMetadataKey', () => {
class ConfigAddress {
@field((type) => String)
public readonly street!: string

@field((type) => String)
public readonly internalCode!: string
}

class ConfigUser {
@field((type) => ConfigAddress)
public readonly address!: ConfigAddress
}

const config = new MagekConfig('test')
config.nonExposedGraphQLMetadataKey[ConfigAddress.name] = ['internalCode']

const { sortFields } = sortFieldsFor(ConfigUser, ConfigAddress, config)

expect(sortFields).to.have.property('street')
expect(sortFields).to.not.have.property('internalCode')
})
})
Loading