Skip to content

Commit

Permalink
content client: add custom transforms
Browse files Browse the repository at this point in the history
  • Loading branch information
matej21 committed May 29, 2024
1 parent 4c069bf commit 4b28a7b
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 59 deletions.
14 changes: 13 additions & 1 deletion build/api/client-content.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,16 @@ export class ContentEntitySelection {
// @internal
constructor(
context: ContentEntitySelectionContext<string>,
selectionSet: GraphQlSelectionSet);
selectionSet: GraphQlSelectionSet,
transformFn?: ((value: any, ctx: ContentTransformContext) => any) | undefined);
// @internal (undocumented)
readonly context: ContentEntitySelectionContext<string>;
// @internal (undocumented)
readonly selectionSet: GraphQlSelectionSet;
// (undocumented)
transform(transform: (value: any, context: ContentTransformContext) => any): ContentEntitySelection;
// @internal (undocumented)
readonly transformFn?: ((value: any, ctx: ContentTransformContext) => any) | undefined;
}

// @public (undocumented)
Expand Down Expand Up @@ -323,6 +328,11 @@ export class ContentQueryBuilder {
upsert(name: string, args: Input.UpsertInput, fields?: EntitySelectionOrCallback): ContentMutation<MutationResult>;
}

// @public (undocumented)
export type ContentTransformContext = {
rootValue: unknown;
};

// @public (undocumented)
export type EntitySelectionAnyArgs = EntitySelectionColumnArgs | EntitySelectionManyArgs | EntitySelectionManyByArgs | EntitySelectionOneArgs;

Expand Down Expand Up @@ -506,6 +516,8 @@ export interface TypedEntitySelection<TSchema extends SchemaTypeLike, TEntityNam
}>;
// @internal (undocumented)
readonly context: ContentEntitySelectionContext<TEntityName>;
// (undocumented)
transform<T>(cb: (value: TValue) => T): TypedEntitySelection<TSchema, TEntityName, TEntity, T>;
}

// @public (undocumented)
Expand Down
125 changes: 74 additions & 51 deletions packages/client-content/src/ContentQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { ContentClientInput, MutationResult, SchemaNames, TransactionResult } from './types'
import {
ContentEntitySelection,
ContentEntitySelectionCallback,
ContentEntitySelectionContext,
ContentMutation,
ContentOperation,
ContentQuery,
} from './nodes'
import { ContentEntitySelection, ContentEntitySelectionCallback, ContentEntitySelectionContext, ContentMutation, ContentOperation, ContentQuery } from './nodes'
import { createListArgs } from './utils/createListArgs'
import { createTypedArgs } from './utils/createTypedArgs'
import { Input } from '@contember/schema'
import { GraphQlField, GraphQlFragmentSpread, GraphQlSelectionSet } from '@contember/graphql-builder'
import { GraphQlField, GraphQlFieldTypedArgs, GraphQlFragmentSpread, GraphQlSelectionSet } from '@contember/graphql-builder'
import { createMutationOperationSet } from './utils/createMutationOperationSet'

export type EntitySelectionOrCallback =
Expand All @@ -22,6 +15,8 @@ export type MutationTransactionOptions = {
deferUniqueConstraints?: boolean
}

type MutationOperation = 'create' | 'update' | 'delete' | 'upsert' | 'transaction';

export class ContentQueryBuilder {
constructor(private readonly schema: SchemaNames) {
}
Expand Down Expand Up @@ -61,9 +56,17 @@ export class ContentQueryBuilder {
const context = this.createContext(name)
const fieldName = `list${name}`
const typedArgs = createListArgs(context.entity, args)
const selectionSet = this.resolveSelectionSet(fields, context)

return new ContentOperation('query', fieldName, typedArgs, selectionSet)
const selection = this.resolveSelection(fields, context)

return new ContentOperation('query', fieldName, typedArgs, selection.selectionSet, it => {
const transformFn = selection.transformFn
if (transformFn) {
return it.map((el: any) => transformFn(el, {
rootValue: it,
}))
}
return it
})
}


Expand All @@ -74,68 +77,83 @@ export class ContentQueryBuilder {
by: `${context.entity.name}UniqueWhere!`,
filter: `${context.entity.name}Where`,
})
const selectionSet = this.resolveSelectionSet(fields, context)
const selection = this.resolveSelection(fields, context)

return new ContentOperation('query', fieldName, typedArgs, selectionSet)
return new ContentOperation('query', fieldName, typedArgs, selection.selectionSet, it => {
if (it && selection.transformFn) {
return selection.transformFn(it, {
rootValue: it,
})
}
return it
})
}


public create(name: string, args: Input.CreateInput, fields?: EntitySelectionOrCallback): ContentMutation<MutationResult> {

const context = this.createContext(name)
const fieldName = `create${name}`
const entity = this.getEntity(name)
const typedArgs = createTypedArgs(args, {
data: `${context.entity.name}CreateInput!`,
data: `${entity.name}CreateInput!`,
})
const selectionSet = this.createMutationSelection('create', fields ? this.resolveSelectionSet(fields, context) : undefined)

return new ContentOperation('mutation', fieldName, typedArgs, selectionSet)
return this.createMutationOperation(name, 'create', typedArgs, fields)
}


public update(name: string, args: Input.UpdateInput, fields?: EntitySelectionOrCallback): ContentMutation<MutationResult> {

const context = this.createContext(name)
const fieldName = `update${name}`
const entity = this.getEntity(name)
const typedArgs = createTypedArgs(args, {
data: `${context.entity.name}UpdateInput!`,
by: `${context.entity.name}UniqueWhere!`,
filter: `${context.entity.name}Where`,
data: `${entity.name}UpdateInput!`,
by: `${entity.name}UniqueWhere!`,
filter: `${entity.name}Where`,
})
const selectionSet = this.createMutationSelection('update', fields ? this.resolveSelectionSet(fields, context) : undefined)

return new ContentOperation('mutation', fieldName, typedArgs, selectionSet)
return this.createMutationOperation(name, 'update', typedArgs, fields)
}


public upsert(name: string, args: Input.UpsertInput, fields?: EntitySelectionOrCallback): ContentMutation<MutationResult> {

const context = this.createContext(name)
const fieldName = `upsert${name}`
const entity = this.getEntity(name)
const typedArgs = createTypedArgs(args, {
update: `${context.entity.name}UpdateInput!`,
create: `${context.entity.name}CreateInput!`,
by: `${context.entity.name}UniqueWhere!`,
filter: `${context.entity.name}Where`,
update: `${entity.name}UpdateInput!`,
create: `${entity.name}CreateInput!`,
by: `${entity.name}UniqueWhere!`,
filter: `${entity.name}Where`,
})

const selectionSet = this.createMutationSelection('upsert', fields ? this.resolveSelectionSet(fields, context) : undefined)

return new ContentOperation('mutation', fieldName, typedArgs, selectionSet)
return this.createMutationOperation(name, 'upsert', typedArgs, fields)
}


public delete(name: string, args: Input.UniqueQueryInput, fields?: EntitySelectionOrCallback): ContentMutation<MutationResult> {

const context = this.createContext(name)
const entity = this.getEntity(name)
const typedArgs = createTypedArgs(args, {
by: `${context.entity.name}UniqueWhere!`,
filter: `${context.entity.name}Where`,
by: `${entity.name}UniqueWhere!`,
filter: `${entity.name}Where`,
})
const fieldName = `delete${name}`
const selectionSet = this.createMutationSelection('delete', fields ? this.resolveSelectionSet(fields, context) : undefined)
return this.createMutationOperation(name, 'delete', typedArgs, fields)
}


private createMutationOperation(name: string, operation: MutationOperation, args: GraphQlFieldTypedArgs, fields?: EntitySelectionOrCallback): ContentMutation<MutationResult> {

const context = this.createContext(name)
const fieldName = `${operation}${name}`
const nodeSelection = fields ? this.resolveSelection(fields, context) : undefined
const selectionSet = this.createMutationSelection(operation, nodeSelection?.selectionSet)

return new ContentOperation('mutation', fieldName, typedArgs, selectionSet)
return new ContentOperation('mutation', fieldName, args, selectionSet, it => {
if (!nodeSelection?.transformFn) {
return it
}
return {
...it,
node: it.node ? nodeSelection.transformFn(it.node, {
rootValue: it.node,
}) : null,
}
})
}

public transaction(
Expand All @@ -162,7 +180,7 @@ export class ContentQueryBuilder {
})
}

private createMutationSelection(operation: 'create' | 'update' | 'delete' | 'upsert' | 'transaction', selection?: GraphQlSelectionSet): GraphQlSelectionSet {
private createMutationSelection(operation: MutationOperation, selection?: GraphQlSelectionSet): GraphQlSelectionSet {
const items: GraphQlSelectionSet = [
new GraphQlField(null, 'ok'),
new GraphQlField(null, 'errorMessage'),
Expand All @@ -182,23 +200,28 @@ export class ContentQueryBuilder {
}


private resolveSelectionSet(
private resolveSelection(
fields: EntitySelectionOrCallback,
context: ContentEntitySelectionContext<string>,
) {
return (typeof fields === 'function' ? fields(new ContentEntitySelection(context, [])) : fields).selectionSet
return (typeof fields === 'function' ? fields(new ContentEntitySelection(context, [])) : fields)
}

private createContext(
name: string,
): ContentEntitySelectionContext<string> {
const entity = this.schema.entities[name]
if (!entity) {
throw new Error(`Entity ${name} not found`)
}
const entity = this.getEntity(name)
return {
entity: entity,
schema: this.schema,
}
}

private getEntity(name: string) {
const entity = this.schema.entities[name]
if (!entity) {
throw new Error(`Entity ${name} not found`)
}
return entity
}
}
47 changes: 43 additions & 4 deletions packages/client-content/src/nodes/ContentEntitySelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export type EntitySelectionAnyArgs =
export type ContentEntitySelectionOrCallback = ContentEntitySelectionCallback | ContentEntitySelection


export type ContentTransformContext = {
rootValue: unknown
}

export class ContentEntitySelection {

/**
Expand All @@ -38,6 +42,8 @@ export class ContentEntitySelection {
public readonly context: ContentEntitySelectionContext<string>,
/** @internal */
public readonly selectionSet: GraphQlSelectionSet,
/** @internal */
public readonly transformFn?: (value: any, ctx: ContentTransformContext) => any,
) {
}

Expand Down Expand Up @@ -93,6 +99,13 @@ export class ContentEntitySelection {
return new ContentEntitySelection(this.context, nodes)
}


transform(transform: (value: any, context: ContentTransformContext) => any): ContentEntitySelection {
return new ContentEntitySelection(this.context, this.selectionSet, !this.transformFn ? transform : (value, ctx) => {
return transform(this.transformFn!(value, ctx), ctx)
})
}

private _column(
name: string,
args: EntitySelectionCommonInput = {},
Expand Down Expand Up @@ -141,7 +154,12 @@ export class ContentEntitySelection {
createListArgs(entity, args),
entitySelection.selectionSet,
)
return this.withField(newObjectField)
const selectionWithField = this.withField(newObjectField)
const nestedTransform = entitySelection.transformFn
if (!nestedTransform) {
return selectionWithField
}
return this.withFieldTransform(alias, it => it.map(nestedTransform))
}


Expand Down Expand Up @@ -188,7 +206,12 @@ export class ContentEntitySelection {
argsWithType,
entitySelection.selectionSet,
)
return this.withField(newObjectField)
const selectionWithField = this.withField(newObjectField)
const nestedTransform = entitySelection.transformFn
if (!nestedTransform) {
return selectionWithField
}
return this.withFieldTransform(alias, (it, ctx) => it !== null ? nestedTransform(it, ctx) : null)
}

private _one(
Expand Down Expand Up @@ -227,14 +250,30 @@ export class ContentEntitySelection {
argsWithType,
entitySelection.selectionSet,
)
return this.withField(newObjectField)
const selectionWithField = this.withField(newObjectField)
const nestedTransform = entitySelection.transformFn
if (!nestedTransform) {
return selectionWithField
}
return this.withFieldTransform(alias, (it, ctx) => it !== null ? nestedTransform(it, ctx) : null)
}


private withField(field: GraphQlField) {
return new ContentEntitySelection(this.context, [
...this.selectionSet,
field,
])
], this.transformFn)
}

private withFieldTransform(alias: string, transform: (value: any, ctx: ContentTransformContext) => any) {
return new ContentEntitySelection(this.context, this.selectionSet, (value, ctx) => {
const transformedValue = transform(value[alias], ctx)
const newValue = {
...value,
[alias]: transformedValue,
}
return this.transformFn ? this.transformFn(newValue, ctx) : newValue
})
}
}
3 changes: 3 additions & 0 deletions packages/client-content/src/nodes/TypedEntitySelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export interface TypedEntitySelection<TSchema extends SchemaTypeLike, TEntityNam
[key in keyof TEntity['columns']]: TEntity['columns'][key]
}>


transform<T>(cb: (value: TValue) => T): TypedEntitySelection<TSchema, TEntityName, TEntity, T>

$<
TNestedValue,
TKey extends (keyof TEntity['columns'] | keyof TEntity['hasMany'] | keyof TEntity['hasManyBy'] | keyof TEntity['hasOne']) & string,
Expand Down
3 changes: 3 additions & 0 deletions packages/client-content/src/utils/createTypedArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const createTypedArgs = <TArgs extends Record<string, any>>(
): GraphQlFieldTypedArgs => {
const typedArgs: GraphQlFieldTypedArgs = {}
for (const key in args) {
if (!types.hasOwnProperty(key)) {
throw new Error(`Unknown argument ${key}`)
}
typedArgs[key] = {
graphQlType: types[key],
value: args[key],
Expand Down
Loading

0 comments on commit 4b28a7b

Please sign in to comment.