graphql#visit TypeScript Examples

The following examples show how to use graphql#visit. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: appsync-json-metadata-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getVisitor = (schema: string, target: 'typescript' | 'javascript' | 'typeDeclaration' = 'javascript'): AppSyncJSONVisitor => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncJSONVisitor(
    builtSchema,
    { directives, target: 'metadata', scalars: TYPESCRIPT_SCALAR_MAP, metadataTarget: target, isTimestampFieldsAdded: true },
    {},
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #2
Source File: Variables.ts    From tql with MIT License 6 votes vote down vote up
buildVariableDefinitions = <T extends SelectionSet<any>>(
  schema: GraphQLSchema,
  root: OperationTypeNode,
  selectionSet: T
): Array<VariableDefinition<any, any>> => {
  const variableDefinitions: VariableDefinition<any, any>[] = [];
  const typeInfo = new TypeInfo(schema);

  // @note need to wrap selectionset in an operation (root) for TypeInfo to track correctly
  const operationDefinition = operation(
    root,
    "",
    selectionSet,
    variableDefinitions
  );

  const visitor = visitWithTypeInfo(typeInfo, {
    [Kind.ARGUMENT]: (node) => {
      const type = typeInfo.getArgument()?.astNode?.type!;

      if (node.value.kind === "Variable") {
        // define the `VariableDefinition`
        variableDefinitions.push(variableDefinition(node.value, type));
      }
    },
  });

  // @todo return from here
  visit(operationDefinition, visitor);

  return variableDefinitions;
}
Example #3
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function withTypenameFieldAddedWhereNeeded(ast: ASTNode) {
  return visit(ast, {
    enter: {
      SelectionSet(node: SelectionSetNode) {
        return {
          ...node,
          selections: node.selections.filter(
            selection => !(selection.kind === 'Field' && (selection as FieldNode).name.value === '__typename')
          ),
        };
      },
    },
    leave(node: ASTNode) {
      if (!(node.kind === 'Field' || node.kind === 'FragmentDefinition')) return undefined;
      if (!node.selectionSet) return undefined;

      if (true) {
        return {
          ...node,
          selectionSet: {
            ...node.selectionSet,
            selections: [typenameField, ...node.selectionSet.selections],
          },
        };
      } else {
        return undefined;
      }
    },
  });
}
Example #4
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function removeClientDirectives(ast: ASTNode) {
  return visit(ast, {
    Field(node: FieldNode): FieldNode | null {
      if (node.directives && node.directives.find(directive => directive.name.value === 'client')) return null;
      return node;
    },
    OperationDefinition: {
      leave(node: OperationDefinitionNode): OperationDefinitionNode | null {
        if (!node.selectionSet.selections.length) return null;
        return node;
      },
    },
  });
}
Example #5
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function removeConnectionDirectives(ast: ASTNode) {
  return visit(ast, {
    Directive(node: DirectiveNode): DirectiveNode | null {
      switch(node.name.value) {
        // TODO: remove reference to 'connection' on transformer vNext release
        case 'connection':
          return null;
        case 'hasOne':
          return null;
        case 'belongsTo':
          return null;
        case 'hasMany':
          return null;
        case 'manyToMany':
          return null;
        default:
          return node;
      }
    },
  });
}
Example #6
Source File: appsync-swift-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getGQLv2Visitor = (
  schema: string,
  selectedType?: string,
  generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code,
  isTimestampFieldsAdded: boolean = true,
  emitAuthProvider: boolean = true,
  generateIndexRules: boolean = true,
  handleListNullabilityTransparently: boolean = true,
  transformerVersion: number = 2,
) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncSwiftVisitor(
    builtSchema,
    {
      directives,
      target: 'swift',
      scalars: SWIFT_SCALAR_MAP,
      isTimestampFieldsAdded,
      emitAuthProvider,
      generateIndexRules,
      handleListNullabilityTransparently,
      transformerVersion: transformerVersion,
    },
    { selectedType, generate },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #7
Source File: appsync-javascript-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getGQLv2Visitor = (
  schema: string,
  selectedType?: string,
  isDeclaration: boolean = false,
  generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code,
  isTimestampFieldsAdded: boolean = false,
): AppSyncModelJavascriptVisitor => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelJavascriptVisitor(
    builtSchema,
    { directives, target: 'javascript', scalars: TYPESCRIPT_SCALAR_MAP, isDeclaration, isTimestampFieldsAdded, transformerVersion: 2 },
    { selectedType, generate },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #8
Source File: appsync-java-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getGQLv2Visitor = (
  schema: string,
  selectedType?: string,
  generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code,
) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelJavaVisitor(
    builtSchema,
    {
      directives,
      target: 'android',
      generate,
      scalars: JAVA_SCALAR_MAP,
      isTimestampFieldsAdded: true,
      handleListNullabilityTransparently: true,
      transformerVersion: 2,
    },
    { selectedType },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #9
Source File: appsync-dart-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getGQLv2Visitor = (
  schema: string,
  selectedType?: string,
  generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code,
  enableDartNullSafety: boolean = true,
) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelDartVisitor(
    builtSchema,
    { directives, target: 'dart', scalars: DART_SCALAR_MAP, enableDartNullSafety, transformerVersion: 2 },
    { selectedType, generate },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #10
Source File: appsync-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
createAndGenerateVisitor = (schema: string, usePipelinedTransformer: boolean = false) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelVisitor(
    builtSchema,
    { directives, target: 'general', isTimestampFieldsAdded: true, usePipelinedTransformer: usePipelinedTransformer },
    { generate: CodeGenGenerateEnum.code },
  );
  visit(ast, { leave: visitor });
  visitor.generate();
  return visitor;
}
Example #11
Source File: appsync-swift-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getVisitor = (
  schema: string,
  selectedType?: string,
  generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code,
  isTimestampFieldsAdded: boolean = true,
  emitAuthProvider: boolean = true,
  generateIndexRules: boolean = true,
  handleListNullabilityTransparently: boolean = true,
  transformerVersion: number = 1
) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncSwiftVisitor(
    builtSchema,
    {
      directives,
      target: 'swift',
      scalars: SWIFT_SCALAR_MAP,
      isTimestampFieldsAdded,
      emitAuthProvider,
      generateIndexRules,
      handleListNullabilityTransparently,
      transformerVersion: transformerVersion
    },
    { selectedType, generate },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #12
Source File: appsync-javascript-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getVisitor = (
  schema: string,
  isDeclaration: boolean = false,
  isTimestampFieldsAdded: boolean = false,
): AppSyncModelJavascriptVisitor => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelJavascriptVisitor(
    builtSchema,
    { directives, target: 'javascript', scalars: TYPESCRIPT_SCALAR_MAP, isDeclaration, isTimestampFieldsAdded },
    {},
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #13
Source File: appsync-java-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getVisitor = (
  schema: string,
  selectedType?: string,
  generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code,
  transformerVersion: number = 1,
) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelJavaVisitor(
    builtSchema,
    {
      directives,
      target: 'android',
      generate,
      scalars: JAVA_SCALAR_MAP,
      isTimestampFieldsAdded: true,
      handleListNullabilityTransparently: true,
      transformerVersion: transformerVersion,
    },
    { selectedType },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #14
Source File: appsync-dart-visitor.test.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
getVisitor = ({
  schema,
  selectedType,
  generate = CodeGenGenerateEnum.code,
  enableDartNullSafety = false,
  enableDartZeroThreeFeatures = false,
  isTimestampFieldsAdded = false,
  transformerVersion = 1,
  dartUpdateAmplifyCoreDependency = false
}: {
  schema: string;
  selectedType?: string;
  generate?: CodeGenGenerateEnum;
  enableDartNullSafety?: boolean;
  enableDartZeroThreeFeatures?: boolean;
  isTimestampFieldsAdded?: boolean;
  transformerVersion?: number;
  dartUpdateAmplifyCoreDependency?: boolean;
}) => {
  const ast = parse(schema);
  const builtSchema = buildSchemaWithDirectives(schema);
  const visitor = new AppSyncModelDartVisitor(
    builtSchema,
    {
      directives,
      target: 'dart',
      scalars: DART_SCALAR_MAP,
      enableDartNullSafety,
      enableDartZeroThreeFeatures,
      isTimestampFieldsAdded,
      transformerVersion,
      dartUpdateAmplifyCoreDependency
    },
    { selectedType, generate },
  );
  visit(ast, { leave: visitor });
  return visitor;
}
Example #15
Source File: relay-edge-types.ts    From graphql-eslint with MIT License 6 votes vote down vote up
function getEdgeTypes(schema: GraphQLSchema): EdgeTypes {
  // We don't want cache edgeTypes on test environment
  // Otherwise edgeTypes will be same for all tests
  if (process.env.NODE_ENV !== 'test' && edgeTypesCache) {
    return edgeTypesCache;
  }
  const edgeTypes: EdgeTypes = new Set();
  const visitor: ASTVisitor = {
    ObjectTypeDefinition(node): void {
      const typeName = node.name.value;
      const hasConnectionSuffix = typeName.endsWith('Connection');
      if (!hasConnectionSuffix) {
        return;
      }
      const edges = node.fields.find(field => field.name.value === 'edges');
      if (edges) {
        const edgesTypeName = getTypeName(edges);
        const edgesType = schema.getType(edgesTypeName);
        if (isObjectType(edgesType)) {
          edgeTypes.add(edgesTypeName);
        }
      }
    },
  };
  const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
  visit(astNode, visitor);

  edgeTypesCache = edgeTypes;
  return edgeTypesCache;
}
Example #16
Source File: no-unused-fields.ts    From graphql-eslint with MIT License 6 votes vote down vote up
function getUsedFields(schema: GraphQLSchema, operations: SiblingOperations): UsedFields {
  // We don't want cache usedFields on test environment
  // Otherwise usedFields will be same for all tests
  if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
    return usedFieldsCache;
  }
  const usedFields: UsedFields = Object.create(null);
  const typeInfo = new TypeInfo(schema);

  const visitor = visitWithTypeInfo(typeInfo, {
    Field(node): false | void {
      const fieldDef = typeInfo.getFieldDef();
      if (!fieldDef) {
        // skip visiting this node if field is not defined in schema
        return false;
      }
      const parentTypeName = typeInfo.getParentType().name;
      const fieldName = node.name.value;

      usedFields[parentTypeName] ??= new Set();
      usedFields[parentTypeName].add(fieldName);
    },
  });

  const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
  for (const { document } of allDocuments) {
    visit(document, visitor);
  }
  usedFieldsCache = usedFields;
  return usedFieldsCache;
}
Example #17
Source File: graphql-js-validation.ts    From graphql-eslint with MIT License 6 votes vote down vote up
getFragmentDefsAndFragmentSpreads = (node: DocumentNode): GetFragmentDefsAndFragmentSpreads => {
  const fragmentDefs = new Set<string>();
  const fragmentSpreads = new Set<string>();

  const visitor: ASTVisitor = {
    FragmentDefinition(node) {
      fragmentDefs.add(node.name.value);
    },
    FragmentSpread(node) {
      fragmentSpreads.add(node.name.value);
    },
  };

  visit(node, visitor);
  return { fragmentDefs, fragmentSpreads };
}
Example #18
Source File: converter.ts    From graphql-eslint with MIT License 5 votes vote down vote up
export function convertToESTree<T extends DocumentNode>(node: T, schema?: GraphQLSchema) {
  const typeInfo = schema ? new TypeInfo(schema) : null;

  const visitor: ASTVisitor = {
    leave(node, key, parent) {
      const leadingComments: Comment[] =
        'description' in node && node.description
          ? [
              {
                type: node.description.block ? 'Block' : 'Line',
                value: node.description.value,
              },
            ]
          : [];

      const calculatedTypeInfo: TypeInformation | Record<string, never> = typeInfo
        ? {
            argument: typeInfo.getArgument(),
            defaultValue: typeInfo.getDefaultValue(),
            directive: typeInfo.getDirective(),
            enumValue: typeInfo.getEnumValue(),
            fieldDef: typeInfo.getFieldDef(),
            inputType: typeInfo.getInputType(),
            parentInputType: typeInfo.getParentInputType(),
            parentType: typeInfo.getParentType(),
            gqlType: typeInfo.getType(),
          }
        : {};

      const rawNode = () => {
        if (parent && key !== undefined) {
          return parent[key];
        }
        return node.kind === Kind.DOCUMENT
          ? <DocumentNode>{
              ...node,
              definitions: node.definitions.map(definition =>
                (definition as unknown as GraphQLESTreeNode<DefinitionNode>).rawNode()
              ),
            }
          : node;
      };

      const commonFields: Omit<GraphQLESTreeNode<typeof node>, 'parent'> = {
        ...node,
        type: node.kind,
        loc: convertLocation(node.loc),
        range: [node.loc.start, node.loc.end],
        leadingComments,
        // Use function to prevent RangeError: Maximum call stack size exceeded
        typeInfo: () => calculatedTypeInfo as any, // Don't know if can fix error
        rawNode,
      };

      return 'type' in node
        ? {
            ...commonFields,
            gqlType: node.type,
          }
        : commonFields;
    },
  };

  return visit(node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor) as GraphQLESTreeNode<T>;
}
Example #19
Source File: index.ts    From graphql-mesh with MIT License 5 votes vote down vote up
transformRequest(executionRequest: ExecutionRequest, delegationContext: DelegationContext): ExecutionRequest {
    const { transformedSchema, rootValue, args, context, info } = delegationContext;
    if (transformedSchema) {
      const errors: GraphQLError[] = [];
      const resolverData: ResolverData = {
        env: process.env,
        root: rootValue,
        args,
        context,
        info,
      };
      const typeInfo = new TypeInfo(transformedSchema);
      let remainingFields = 0;
      const newDocument = visit(
        executionRequest.document,
        visitWithTypeInfo(typeInfo, {
          Field: () => {
            const parentType = typeInfo.getParentType();
            const fieldDef = typeInfo.getFieldDef();
            const path = `${parentType.name}.${fieldDef.name}`;
            const rateLimitConfig = this.pathRateLimitDef.get(path);
            if (rateLimitConfig) {
              const identifier = stringInterpolator.parse(rateLimitConfig.identifier, resolverData);
              const mapKey = `${identifier}-${path}`;
              let remainingTokens = this.tokenMap.get(mapKey);

              if (remainingTokens == null) {
                remainingTokens = rateLimitConfig.max;
                const timeout = setTimeout(() => {
                  this.tokenMap.delete(mapKey);
                  this.timeouts.delete(timeout);
                }, rateLimitConfig.ttl);
                this.timeouts.add(timeout);
              }

              if (remainingTokens === 0) {
                errors.push(new GraphQLError(`Rate limit of "${path}" exceeded for "${identifier}"`));
                // Remove this field from the selection set
                return null;
              } else {
                this.tokenMap.set(mapKey, remainingTokens - 1);
              }
            }
            remainingFields++;
            return false;
          },
        })
      );
      if (remainingFields === 0) {
        if (errors.length === 1) {
          throw errors[0];
        } else if (errors.length > 0) {
          throw new AggregateError(errors);
        }
      }
      this.errors.set(delegationContext, errors);
      return {
        ...executionRequest,
        document: newDocument,
      };
    }
    return executionRequest;
  }
Example #20
Source File: index.ts    From graphql-mesh with MIT License 5 votes vote down vote up
export default function useMeshLiveQuery(options: MeshPluginOptions<YamlConfig.LiveQueryConfig>): Plugin {
  const liveQueryInvalidationFactoryMap = new Map<string, ResolverDataBasedFactory<string>[]>();
  options.logger.debug(`Creating Live Query Store`);
  const liveQueryStore = new InMemoryLiveQueryStore({
    includeIdentifierExtension: true,
  });
  options.liveQueryInvalidations?.forEach(liveQueryInvalidation => {
    const rawInvalidationPaths = liveQueryInvalidation.invalidate;
    const factories = rawInvalidationPaths.map(rawInvalidationPath =>
      getInterpolatedStringFactory(rawInvalidationPath)
    );
    liveQueryInvalidationFactoryMap.set(liveQueryInvalidation.field, factories);
  });
  return useEnvelop(
    envelop({
      plugins: [
        useLiveQuery({ liveQueryStore }),
        {
          onExecute(onExecuteParams) {
            if (!onExecuteParams.args.schema.getDirective('live')) {
              options.logger.warn(`You have to add @live directive to additionalTypeDefs to enable Live Queries
See more at https://www.graphql-mesh.com/docs/recipes/live-queries`);
            }
            return {
              onExecuteDone({ args: executionArgs, result }) {
                queueMicrotask(() => {
                  const { schema, document, operationName, rootValue, contextValue } = executionArgs;
                  const operationAST = getOperationAST(document, operationName);
                  if (!operationAST) {
                    throw new Error(`Operation couldn't be found`);
                  }
                  const typeInfo = new TypeInfo(schema);
                  visit(
                    operationAST,
                    visitWithTypeInfo(typeInfo, {
                      Field: () => {
                        const parentType = typeInfo.getParentType();
                        const fieldDef = typeInfo.getFieldDef();
                        const path = `${parentType.name}.${fieldDef.name}`;
                        if (liveQueryInvalidationFactoryMap.has(path)) {
                          const invalidationPathFactories = liveQueryInvalidationFactoryMap.get(path);
                          const invalidationPaths = invalidationPathFactories.map(invalidationPathFactory =>
                            invalidationPathFactory({
                              root: rootValue,
                              context: contextValue,
                              env: process.env,
                              result,
                            })
                          );
                          liveQueryStore
                            .invalidate(invalidationPaths)
                            .catch((e: Error) => options.logger.warn(`Invalidation failed for ${path}: ${e.message}`));
                        }
                      },
                    })
                  );
                });
              },
            };
          },
        },
      ],
    })
  );
}
Example #21
Source File: plugin.ts    From amplify-codegen with Apache License 2.0 5 votes vote down vote up
plugin: PluginFunction<RawAppSyncModelConfig> = (
  schema: GraphQLSchema,
  rawDocuments: Types.DocumentFile[],
  config: RawAppSyncModelConfig,
) => {
  let visitor;
  switch (config.target) {
    case 'swift':
      visitor = new AppSyncSwiftVisitor(schema, config, {
        selectedType: config.selectedType,
        generate: config.generate,
      });
      break;
    case 'java':
      visitor = new AppSyncModelJavaVisitor(schema, config, {
        selectedType: config.selectedType,
        generate: config.generate,
      });
      break;
    case 'metadata':
      visitor = new AppSyncJSONVisitor(schema, config, {});
      break;
    case 'typescript':
      visitor = new AppSyncModelTypeScriptVisitor(schema, config, {});
      break;
    case 'javascript':
      visitor = new AppSyncModelJavascriptVisitor(schema, config, {});
      break;
    case 'dart':
      visitor = new AppSyncModelDartVisitor(schema, config, {
        selectedType: config.selectedType,
        generate: config.generate,
      });
      break;
    default:
      return '';
  }
  if (schema) {
    const schemaStr = printSchemaWithDirectives(schema);
    const node = parse(schemaStr);
    visit(node, {
      leave: visitor,
    });
    return visitor.generate();
  }
  return '';
}
Example #22
Source File: no-unreachable-types.ts    From graphql-eslint with MIT License 5 votes vote down vote up
function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
  // We don't want cache reachableTypes on test environment
  // Otherwise reachableTypes will be same for all tests
  if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
    return reachableTypesCache;
  }
  const reachableTypes: ReachableTypes = new Set();

  const collect = (node: ASTNode): false | void => {
    const typeName = getTypeName(node);
    if (reachableTypes.has(typeName)) {
      return;
    }
    reachableTypes.add(typeName);
    const type = schema.getType(typeName) || schema.getDirective(typeName);

    if (isInterfaceType(type)) {
      const { objects, interfaces } = schema.getImplementations(type);
      for (const { astNode } of [...objects, ...interfaces]) {
        visit(astNode, visitor);
      }
    } else if (type.astNode) {
      // astNode can be undefined for ID, String, Boolean
      visit(type.astNode, visitor);
    }
  };

  const visitor: ASTVisitor = {
    InterfaceTypeDefinition: collect,
    ObjectTypeDefinition: collect,
    InputValueDefinition: collect,
    UnionTypeDefinition: collect,
    FieldDefinition: collect,
    Directive: collect,
    NamedType: collect,
  };

  for (const type of [
    schema, // visiting SchemaDefinition node
    schema.getQueryType(),
    schema.getMutationType(),
    schema.getSubscriptionType(),
  ]) {
    // if schema don't have Query type, schema.astNode will be undefined
    if (type?.astNode) {
      visit(type.astNode, visitor);
    }
  }
  reachableTypesCache = reachableTypes;
  return reachableTypesCache;
}
Example #23
Source File: render.ts    From tql with MIT License 5 votes vote down vote up
render = (sdl: string): string => {
  const ast = parse(sdl, { noLocation: true });
  const schema = buildSchema(sdl);

  const transforms = [
    typeTransform(ast, schema),
    selectorInterfaceTransform(ast, schema),
  ];

  // additive transforms
  const results = transforms.map(
    (vistor) => visit(ast, vistor)
  ) as unknown as Array<{ readonly definitions: Code[] }> ;

  const types = Object.values(schema.getTypeMap()).filter(
    (type) => !type.name.startsWith("__")
  );

  const enumValues = new Set(
    Object.values(schema.getTypeMap())
      .filter((type) => type instanceof GraphQLEnumType)
      .flatMap((type) =>
        (type as GraphQLEnumType).getValues().map((value) => value.value)
      )
  );

  const ENUMS = `
    Object.freeze({
      ${Array.from(enumValues)
        .map((value) => `${value}: true`)
        .join(",\n")}
    } as const)
  `;

  const typeMap = `
    export interface ISchema {
      ${types.map(printType).join("\n")}
    }
  `;

  const source =
    `
    import { buildASTSchema, Kind, OperationTypeNode } from 'graphql'

    import {
      TypeConditionError,
      NamedType,
      Field,
      InlineFragment,
      Argument,
      Variable,
      Selection,
      SelectionSet,
      SelectionBuilder,
      namedType,
      field,
      inlineFragment,
      argument,
      selectionSet
     } from '@timkendall/tql'

     export type { Result, SelectionResult, Variables } from '@timkendall/tql'
     export { $ } from '@timkendall/tql'

     ` +
    `
    export const SCHEMA = buildASTSchema(${stringifyAST(ast)})

    export const ENUMS = ${ENUMS}

    ${typeMap}
  ` +
    results
      .flatMap((result) =>
        result.definitions.map((code) => code.toCodeString())
      )
      .join("\n");

  return prettier.format(source, { parser: "typescript" });
}
Example #24
Source File: appsync-visitor.test.ts    From amplify-codegen with Apache License 2.0 4 votes vote down vote up
describe('AppSyncModelVisitor', () => {
  it('should support schema with id', () => {
    const schema = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: String!
        content: String!
      }
    `;
    const ast = parse(schema);
    const builtSchema = buildSchemaWithDirectives(schema);
    const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
    visit(ast, { leave: visitor });
    expect(visitor.models.Post).toBeDefined();

    const postFields = visitor.models.Post.fields;
    expect(postFields[0].name).toEqual('id');
    expect(postFields[0].type).toEqual('ID');
    expect(postFields[0].isNullable).toEqual(false);
    expect(postFields[0].isList).toEqual(false);
  });

  it('should throw error when schema has id of Non ID type', () => {
    const schema = /* GraphQL */ `
      type Post @model {
        id: String!
        title: String!
        content: String!
      }
    `;
    const ast = parse(schema);
    const builtSchema = buildSchemaWithDirectives(schema);
    const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
    expect(() => visit(ast, { leave: visitor })).toThrowError();
  });
  it('should have id as the first field to ensure arity of constructors', () => {
    const schema = /* GraphQL */ `
      type Post @model {
        title: String!
        content: String!
        id: ID!
      }
    `;
    const ast = parse(schema);
    const builtSchema = buildSchemaWithDirectives(schema);
    const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
    visit(ast, { leave: visitor });
    const postFields = visitor.models.Post.fields;
    expect(postFields[0].name).toEqual('id');
    expect(postFields[0].type).toEqual('ID');
    expect(postFields[0].isNullable).toEqual(false);
    expect(postFields[0].isList).toEqual(false);
  });

  it('should generate different version if field required attribute is different', () => {
    const schemaNotRequired = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: String
      }
    `;
    const visitorNotRequired = createAndGenerateVisitor(schemaNotRequired);

    const schemaRequired = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: String!
      }
    `;
    const visitorRequired = createAndGenerateVisitor(schemaRequired);

    const notRequiredVersion = (visitorNotRequired as any).computeVersion();
    const requiredVersion = (visitorRequired as any).computeVersion();

    expect(notRequiredVersion).not.toBe(requiredVersion);
  });

  it('should generate different version if field array attribute is different', () => {
    const schemaNoArray = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: String
      }
    `;
    const visitorNoArray = createAndGenerateVisitor(schemaNoArray);

    const schemaWithArray = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: [String]
      }
    `;
    const visitorWithArray = createAndGenerateVisitor(schemaWithArray);

    const noArrayVersion = (visitorNoArray as any).computeVersion();
    const withArrayVersion = (visitorWithArray as any).computeVersion();

    expect(noArrayVersion).not.toBe(withArrayVersion);
  });

  it('should generate different version if field array has required items', () => {
    const schemaNotRequired = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: [String]
      }
    `;
    const visitorNotRequired = createAndGenerateVisitor(schemaNotRequired);

    const schemaRequired = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: [String!]
      }
    `;
    const visitorRequired = createAndGenerateVisitor(schemaRequired);

    const notRequiredVersion = (visitorNotRequired as any).computeVersion();
    const requiredVersion = (visitorRequired as any).computeVersion();

    expect(notRequiredVersion).not.toBe(requiredVersion);
  });

  it('should generate different version if field array changes to required', () => {
    const schemaNotRequired = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: [String]
      }
    `;
    const visitorNotRequired = createAndGenerateVisitor(schemaNotRequired);

    const schemaRequired = /* GraphQL */ `
      type Post @model {
        id: ID!
        title: [String]!
      }
    `;
    const visitorRequired = createAndGenerateVisitor(schemaRequired);

    const notRequiredVersion = (visitorNotRequired as any).computeVersion();
    const requiredVersion = (visitorRequired as any).computeVersion();

    expect(notRequiredVersion).not.toBe(requiredVersion);
  });

  describe(' 2 Way Connection', () => {
    describe('with connection name', () => {
      const schema = /* GraphQL */ `
        type Post @model {
          id: ID!
          title: String!
          content: String
          comments: [Comment] @connection(name: "PostComment")
        }

        type Comment @model {
          id: ID!
          comment: String!
          post: Post @connection(name: "PostComment")
        }
      `;
      it('one to many connection', () => {
        const ast = parse(schema);
        const builtSchema = buildSchemaWithDirectives(schema);
        const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
        visit(ast, { leave: visitor });
        visitor.generate();
        const commentsField = visitor.models.Post.fields.find(f => f.name === 'comments');
        const postField = visitor.models.Comment.fields.find(f => f.name === 'post');
        expect(commentsField).toBeDefined();
        expect(commentsField!.connectionInfo).toBeDefined();
        const connectionInfo = (commentsField!.connectionInfo as any) as CodeGenFieldConnectionHasMany;
        expect(connectionInfo.kind).toEqual(CodeGenConnectionType.HAS_MANY);
        expect(connectionInfo.associatedWith).toEqual(postField);
        expect(connectionInfo.connectedModel).toEqual(visitor.models.Comment);
      });

      it('many to one connection', () => {
        const ast = parse(schema);
        const builtSchema = buildSchemaWithDirectives(schema);
        const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
        visit(ast, { leave: visitor });
        visitor.generate();
        const commentsField = visitor.models.Post.fields.find(f => f.name === 'comments');
        const postField = visitor.models.Comment.fields.find(f => f.name === 'post');
        expect(postField).toBeDefined();
        expect(postField!.connectionInfo).toBeDefined();
        const connectionInfo = (postField!.connectionInfo as any) as CodeGenFieldConnectionBelongsTo;
        expect(connectionInfo.kind).toEqual(CodeGenConnectionType.BELONGS_TO);
        expect(connectionInfo.connectedModel).toEqual(visitor.models.Post);
      });
    });
    describe('connection with fields argument', () => {
      const schema = /* GraphQL */ `
        type Post @model {
          id: ID!
          title: String!
          content: String
          comments: [Comment] @connection(fields: ["id"])
        }

        type Comment @model {
          id: ID!
          comment: String!
          postId: ID!
          post: Post @connection(fields: ["postId"])
        }
      `;

      it('one to many connection', () => {
        const ast = parse(schema);
        const builtSchema = buildSchemaWithDirectives(schema);
        const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
        visit(ast, { leave: visitor });
        visitor.generate();
        const commentsField = visitor.models.Post.fields.find(f => f.name === 'comments');
        const commentIdField = visitor.models.Post.fields.find(f => f.name === 'id');
        const postField = visitor.models.Comment.fields.find(f => f.name === 'post');
        expect(commentsField).toBeDefined();
        expect(commentsField!.connectionInfo).toBeDefined();
        const connectionInfo = (commentsField!.connectionInfo as any) as CodeGenFieldConnectionHasMany;
        expect(connectionInfo.kind).toEqual(CodeGenConnectionType.HAS_MANY);
        expect(connectionInfo.associatedWith).toEqual(commentIdField);
        expect(connectionInfo.connectedModel).toEqual(visitor.models.Comment);
      });

      it('many to one connection', () => {
        const ast = parse(schema);
        const builtSchema = buildSchemaWithDirectives(schema);
        const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
        visit(ast, { leave: visitor });
        visitor.generate();

        const postField = visitor.models.Comment.fields.find(f => f.name === 'post');
        expect(postField).toBeDefined();
        expect(postField!.connectionInfo).toBeDefined();
        const connectionInfo = (postField!.connectionInfo as any) as CodeGenFieldConnectionBelongsTo;
        expect(connectionInfo.kind).toEqual(CodeGenConnectionType.BELONGS_TO);
        expect(connectionInfo.connectedModel).toEqual(visitor.models.Post);
      });
    });
  });

  describe('one way connection', () => {
    it('should not include a comments in Post when comments field does not have connection directive', () => {
      const schema = /* GraphQL */ `
        type Post @model {
          id: ID!
          title: String!
          content: String
          comments: [Comment]
        }

        type Comment @model {
          id: ID!
          comment: String!
          post: Post @connection
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
      visit(ast, { leave: visitor });
      visitor.generate();
      const postFields = visitor.models.Post.fields.map(field => field.name);
      expect(postFields).not.toContain('comments');
    });

    it('should not include a post when post field in Comment when post does not have connection directive', () => {
      const schema = /* GraphQL */ `
        type Post @model {
          id: ID!
          title: String!
          content: String
          comments: [Comment] @connection
        }

        type Comment @model {
          id: ID!
          comment: String!
          post: Post
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
      visit(ast, { leave: visitor });
      visitor.generate();
      const commentsField = visitor.models.Comment.fields.map(field => field.name);
      expect(commentsField).not.toContain('post');
      expect(commentsField).toContain('postCommentsId'); // because of connection from Post.comments
    });

    it('should generate projectTeamId connection field for hasOne directive in the parent object', () => {
      const schema = /* GraphQL */ `
        type Project @model {
          id: ID!
          name: String
          team: Team @hasOne
        }
        type Team @model {
          id: ID!
          name: String!
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      const visitor = new AppSyncModelVisitor(
        builtSchema,
        { directives, target: 'typescript', generate: CodeGenGenerateEnum.code, usePipelinedTransformer: true },
        {},
      );
      visit(ast, { leave: visitor });
      visitor.generate();
      const projectTeamIdField = visitor.models.Project.fields.find(field => {
        return field.name === 'projectTeamId';
      });
      expect(projectTeamIdField).toBeDefined();
      expect(projectTeamIdField.isNullable).toBeTruthy();
    });
  });

  describe('index directives', () => {
    it('processes index directive', () => {
      const schema = /* GraphQL */ `
        type Project @model {
          id: ID!
          name: String @index(name: "nameIndex", sortKeyFields: ["team"])
          team: Team
        }

        type Team @model {
          id: ID!
          name: String! @index(name: "teamNameIndex")
        }
      `;
      const visitor = createAndGenerateVisitor(schema, true);
      visitor.generate();
      const projectKeyDirective = visitor.models.Project.directives.find(directive => directive.name === 'key');
      expect(projectKeyDirective).toEqual({
        name: 'key',
        arguments: {
          name: 'nameIndex',
          fields: ['name', 'team'],
        },
      });
      const teamKeyDirective = visitor.models.Team.directives.find(directive => directive.name === 'key');
      expect(teamKeyDirective).toEqual({
        name: 'key',
        arguments: {
          name: 'teamNameIndex',
          fields: ['name'],
        },
      });
    });

    it('processes primaryKey directive', () => {
      const schema = /* GraphQL */ `
        type Project @model {
          id: ID!
          name: String @primaryKey(sortKeyFields: ["team"])
          team: Team
        }

        type Team @model {
          id: ID!
          name: String! @primaryKey
        }
      `;
      const visitor = createAndGenerateVisitor(schema, true);
      visitor.generate();
      const projectKeyDirective = visitor.models.Project.directives.find(directive => directive.name === 'key');
      expect(projectKeyDirective).toEqual({
        name: 'key',
        arguments: {
          fields: ['name', 'team'],
        },
      });
      const teamKeyDirective = visitor.models.Team.directives.find(directive => directive.name === 'key');
      expect(teamKeyDirective).toEqual({
        name: 'key',
        arguments: {
          fields: ['name'],
        },
      });
    });

    it('processes index with queryField', () => {
      const schema = /* GraphQL */ `
        type Project @model {
          id: ID!
          name: String @index(name: "nameIndex", queryField: "myQuery")
        }
      `;
      const visitor = createAndGenerateVisitor(schema, true);
      visitor.generate();
      const projectKeyDirective = visitor.models.Project.directives.find(directive => directive.name === 'key');
      expect(projectKeyDirective).toEqual({
        name: 'key',
        arguments: {
          name: 'nameIndex',
          queryField: 'myQuery',
          fields: ['name'],
        },
      });
    });
  });

  describe('auth directive', () => {
    it('should process auth with owner authorization', () => {
      const schema = /* GraphQL */ `
        type Post @searchable @model @auth(rules: [{ allow: owner }]) {
          id: ID!
          title: String!
          content: String
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
      visit(ast, { leave: visitor });
      visitor.generate();
      const postModel = visitor.models.Post;
      const authDirective = postModel.directives.find(d => d.name === 'auth');
      expect(authDirective).toBeDefined();
      const authRules = authDirective!.arguments.rules;
      expect(authRules).toBeDefined();
      expect(authRules).toHaveLength(1);
      const ownerRule = authRules[0];
      expect(ownerRule).toBeDefined();

      expect(ownerRule.provider).toEqual('userPools');
      expect(ownerRule.identityClaim).toEqual('cognito:username');
      expect(ownerRule.ownerField).toEqual('owner');
      expect(ownerRule.operations).toEqual(['create', 'update', 'delete', 'read']);
    });

    it('should process group with owner authorization', () => {
      const schema = /* GraphQL */ `
        type Post @model @searchable @auth(rules: [{ allow: groups, groups: ["admin", "moderator"] }]) {
          id: ID!
          title: String!
          content: String
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
      visit(ast, { leave: visitor });
      visitor.generate();
      const postModel = visitor.models.Post;
      const authDirective = postModel.directives.find(d => d.name === 'auth');
      expect(authDirective).toBeDefined();
      const authRules = authDirective!.arguments.rules;
      expect(authRules).toBeDefined();
      expect(authRules).toHaveLength(1);
      const groupRule = authRules[0];
      expect(groupRule).toBeDefined();
      expect(groupRule.provider).toEqual('userPools');
      expect(groupRule.groupClaim).toEqual('cognito:groups');
      expect(groupRule.operations).toEqual(['create', 'update', 'delete', 'read']);
    });

    it('should process field level auth with default owner authorization', () => {
      const schema = /* GraphQL*/ `
        type Employee @model
          @auth(rules: [
              { allow: owner },
              { allow: groups, groups: ["Admins"] }
          ]) {
            id: ID!
            name: String!
            address: String!
            ssn: String @auth(rules: [{allow: owner}])
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
      visit(ast, { leave: visitor });
      visitor.generate();

      const ssnField = visitor.models.Employee.fields.find(f => f.name === 'ssn');
      const authDirective = ssnField?.directives.find(d => d.name === 'auth');
      expect(authDirective).toBeDefined();
      const authRules = authDirective!.arguments.rules;
      expect(authRules).toBeDefined();
      expect(authRules).toHaveLength(1);
      const ownerRule = authRules[0];
      expect(ownerRule).toBeDefined();

      expect(ownerRule.provider).toEqual('userPools');
      expect(ownerRule.identityClaim).toEqual('cognito:username');
      expect(ownerRule.ownerField).toEqual('owner');
      expect(ownerRule.operations).toEqual(['create', 'update', 'delete', 'read']);
    });
  });
  describe('model less type', () => {
    let visitor: AppSyncModelVisitor;
    beforeEach(() => {
      const schema = /* GraphQL */ `
        type Metadata {
          authorName: String!
          tags: [String]
          rating: Int!
        }
      `;
      const ast = parse(schema);
      const builtSchema = buildSchemaWithDirectives(schema);
      visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {});
      visit(ast, { leave: visitor });
      visitor.generate();
    });

    it('should support types without model', () => {
      const metadataType = visitor.nonModels.Metadata;

      expect(metadataType).toBeDefined();
      const metadataFields = metadataType.fields;

      expect(metadataFields[0].name).toEqual('authorName');
      expect(metadataFields[0].type).toEqual('String');
      expect(metadataFields[0].isNullable).toEqual(false);
      expect(metadataFields[0].isList).toEqual(false);

      expect(metadataFields[1].name).toEqual('tags');
      expect(metadataFields[1].type).toEqual('String');
      expect(metadataFields[1].isNullable).toEqual(true);
      expect(metadataFields[1].isList).toEqual(true);

      expect(metadataFields[2].name).toEqual('rating');
      expect(metadataFields[2].type).toEqual('Int');
      expect(metadataFields[2].isNullable).toEqual(false);
      expect(metadataFields[2].isList).toEqual(false);
    });
  });
  describe('timestamps for model directive', () => {
    it('should add timestamps fields in implicit cases', () => {
      const schema = /* GraphQL */ `
        type Post @model {
          id: ID!
        }
      `;
      const createdAtFieldObj = {
        name: 'createdAt',
        type: 'AWSDateTime',
        isList: false,
        isNullable: true,
        isReadOnly: true,
      };
      const updatedAtFieldObj = {
        name: 'updatedAt',
        type: 'AWSDateTime',
        isList: false,
        isNullable: true,
        isReadOnly: true,
      };
      const visitor = createAndGenerateVisitor(schema);
      expect(visitor.models.Post).toBeDefined();

      const postFields = visitor.models.Post.fields;
      const createdAtField = postFields.find(field => field.name === 'createdAt');
      expect(createdAtField).toMatchObject(createdAtFieldObj);
      const updatedAtField = postFields.find(field => field.name === 'updatedAt');
      expect(updatedAtField).toMatchObject(updatedAtFieldObj);
    });
    it('should add user defined timestamp fields in model directives', () => {
      const schema = /* GraphQL */ `
        type Post @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) {
          id: ID!
        }
      `;
      const createdAtFieldObj = {
        name: 'createdOn',
        type: 'AWSDateTime',
        isList: false,
        isNullable: true,
        isReadOnly: true,
      };
      const updatedAtFieldObj = {
        name: 'updatedOn',
        type: 'AWSDateTime',
        isList: false,
        isNullable: true,
        isReadOnly: true,
      };
      const visitor = createAndGenerateVisitor(schema);
      expect(visitor.models.Post).toBeDefined();

      const postFields = visitor.models.Post.fields;
      const createdAtField = postFields.find(field => field.name === 'createdOn');
      expect(createdAtField).toMatchObject(createdAtFieldObj);
      const updatedAtField = postFields.find(field => field.name === 'updatedOn');
      expect(updatedAtField).toMatchObject(updatedAtFieldObj);
    });
    it('should not override original fields if user define them explicitly in schema', () => {
      const schema = /* GraphQL */ `
        type Post @model {
          id: ID!
          createdAt: AWSDateTime!
          updatedAt: AWSDateTime!
        }
      `;
      const createdAtFieldObj = {
        name: 'createdAt',
        type: 'AWSDateTime',
        isList: false,
        isNullable: false,
      };
      const updatedAtFieldObj = {
        name: 'updatedAt',
        type: 'AWSDateTime',
        isList: false,
        isNullable: false,
      };
      const visitor = createAndGenerateVisitor(schema);
      expect(visitor.models.Post).toBeDefined();

      const postFields = visitor.models.Post.fields;
      const createdAtField = postFields.find(field => field.name === 'createdAt');
      expect(createdAtField).toMatchObject(createdAtFieldObj);
      expect(createdAtField.isReadOnly).not.toBeDefined();
      const updatedAtField = postFields.find(field => field.name === 'updatedAt');
      expect(updatedAtField).toMatchObject(updatedAtFieldObj);
    });
    it('should not override original fields if users define them explicitly in schema and use timestamps params in @model', () => {
      const schema = /* GraphQL */ `
        type Post @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) {
          id: ID!
          createdOn: AWSDateTime!
          updatedOn: AWSDateTime!
        }
      `;
      const createdAtFieldObj = {
        name: 'createdOn',
        type: 'AWSDateTime',
        isList: false,
        isNullable: false,
      };
      const updatedAtFieldObj = {
        name: 'updatedOn',
        type: 'AWSDateTime',
        isList: false,
        isNullable: false,
      };
      const visitor = createAndGenerateVisitor(schema);
      expect(visitor.models.Post).toBeDefined();

      const postFields = visitor.models.Post.fields;
      const createdAtField = postFields.find(field => field.name === 'createdOn');
      expect(createdAtField).toMatchObject(createdAtFieldObj);
      expect(createdAtField.isReadOnly).not.toBeDefined();
      const updatedAtField = postFields.find(field => field.name === 'updatedOn');
      expect(updatedAtField).toMatchObject(updatedAtFieldObj);
    });
    it('should not generate timestamp fields if "timestamps:null" is defined in @model', () => {
      const schema = /* GraphQL */ `
        type Post @model(timestamps: null) {
          id: ID!
        }
      `;
      const visitor = createAndGenerateVisitor(schema);
      expect(visitor.models.Post).toBeDefined();

      const postFields = visitor.models.Post.fields;
      const createdAtField = postFields.find(field => field.name === 'createdAt');
      expect(createdAtField).not.toBeDefined();
      const updatedAtField = postFields.find(field => field.name === 'updatedAt');
      expect(updatedAtField).not.toBeDefined();
    });
  });

  describe('manyToMany testing', () => {
    let simpleManyToManySchema;
    let simpleManyModelMap;
    let transformedSimpleManyModelMap;
    let manyToManyModelNameSchema;

    beforeEach(() => {
      simpleManyToManySchema = /* GraphQL */ `
        type Human @model {
          governmentID: ID! @primaryKey
          pets: [Animal] @manyToMany(relationName: "PetFriend")
        }

        type Animal @model {
          animalTag: ID!
          humanFriend: [Human] @manyToMany(relationName: "PetFriend")
        }
      `;

      manyToManyModelNameSchema = /* GraphQL */ `
        type ModelA @model {
          models: [ModelB] @manyToMany(relationName: "Models")
        }

        type ModelB @model {
          models: [ModelA] @manyToMany(relationName: "Models")
        }
      `;

      simpleManyModelMap = {
        Human: {
          name: 'Human',
          type: 'model',
          directives: [],
          fields: [
            {
              type: 'ID',
              isNullable: false,
              isList: false,
              name: 'governmentID',
              directives: [{ name: 'primaryKey', arguments: {} }],
            },
            {
              type: 'Animal',
              isNullable: true,
              isList: true,
              name: 'pets',
              directives: [{ name: 'manyToMany', arguments: { relationName: 'PetFriend' } }],
            },
          ],
        },
        Animal: {
          name: 'Animal',
          type: 'model',
          directives: [],
          fields: [
            {
              type: 'ID',
              isNullable: false,
              isList: false,
              name: 'animalTag',
              directives: [],
            },
            {
              type: 'Human',
              isNullable: true,
              isList: true,
              name: 'postID',
              directives: [{ name: 'manyToMany', arguments: { relationName: 'PetFriend' } }],
            },
          ],
        },
      };
    });

    transformedSimpleManyModelMap = {
      Human: {
        name: 'Human',
        type: 'model',
        directives: [{ name: 'model', arguments: {} }],
        fields: [
          {
            type: 'ID',
            isNullable: false,
            isList: false,
            name: 'governmentID',
            directives: [{ name: 'primaryKey', arguments: {} }],
          },
          {
            type: 'PetFriend',
            isNullable: true,
            isList: true,
            name: 'pets',
            directives: [{ name: 'hasMany', arguments: { fields: ['governmentID'] } }],
          },
        ],
      },
      Animal: {
        name: 'Animal',
        type: 'model',
        directives: [{ name: 'model', arguments: {} }],
        fields: [
          {
            type: 'ID',
            isNullable: false,
            isList: false,
            name: 'animalTag',
            directives: [],
          },
          {
            type: 'PetFriend',
            isNullable: true,
            isList: true,
            name: 'postID',
            directives: [{ name: 'hasMany', arguments: { fields: ['id'] } }],
          },
        ],
      },
      PetFriend: {
        name: 'PetFriend',
        type: 'model',
        directives: [{ name: 'model', arguments: {} }],
        fields: [
          {
            type: 'ID',
            isNullable: false,
            isList: false,
            name: 'id',
            directives: [],
          },
          {
            type: 'ID',
            isNullable: false,
            isList: false,
            name: 'humanID',
            directives: [{ name: 'index', arguments: { name: 'byHuman', sortKeyFields: ['animalID'] } }],
          },
          {
            type: 'ID',
            isNullable: false,
            isList: false,
            name: 'animalID',
            directives: [{ name: 'index', arguments: { name: 'byAnimal', sortKeyFields: ['humanID'] } }],
          },
          {
            type: 'Human',
            isNullable: false,
            isList: false,
            name: 'human',
            directives: [{ name: 'belongsTo', arguments: { fields: ['humanID'] } }],
          },
          {
            type: 'Animal',
            isNullable: false,
            isList: false,
            name: 'animal',
            directives: [{ name: 'belongsTo', arguments: { fields: ['humanID'] } }],
          },
        ],
      },
    };

    it('Should correctly convert the model map of a simple manyToMany', () => {
      const visitor = createAndGeneratePipelinedTransformerVisitor(simpleManyToManySchema);

      expect(visitor.models.Human.fields.length).toEqual(5);
      expect(visitor.models.Human.fields[2].directives[0].name).toEqual('hasMany');
      expect(visitor.models.Human.fields[2].directives[0].arguments.fields.length).toEqual(1);
      expect(visitor.models.Human.fields[2].directives[0].arguments.fields[0]).toEqual('governmentID');
      expect(visitor.models.Human.fields[2].directives[0].arguments.indexName).toEqual('byHuman');
      expect(visitor.models.PetFriend).toBeDefined();
      expect(visitor.models.PetFriend.fields.length).toEqual(5);
      expect(visitor.models.PetFriend.fields[2].directives[0].name).toEqual('belongsTo');
      expect(visitor.models.PetFriend.fields[2].directives[0].arguments.fields.length).toEqual(1);
      expect(visitor.models.PetFriend.fields[2].directives[0].arguments.fields[0]).toEqual('animalID');
      expect(visitor.models.Animal.fields.length).toEqual(5);
      expect(visitor.models.Animal.fields[2].type).toEqual('PetFriend');
      expect(visitor.models.Animal.fields[2].directives.length).toEqual(1);
      expect(visitor.models.Animal.fields[2].directives[0].name).toEqual('hasMany');
      expect(visitor.models.Animal.fields[2].directives[0].arguments.fields.length).toEqual(1);
      expect(visitor.models.Animal.fields[2].directives[0].arguments.fields[0]).toEqual('id');
      expect(visitor.models.Animal.fields[2].directives[0].arguments.indexName).toEqual('byAnimal');
    });

    it('Should correctly field names for many to many join table', () => {
      const visitor = createAndGeneratePipelinedTransformerVisitor(manyToManyModelNameSchema);

      expect(visitor.models.ModelA.fields.length).toEqual(4);
      expect(visitor.models.ModelA.fields[1].directives[0].name).toEqual('hasMany');
      expect(visitor.models.ModelA.fields[1].directives[0].arguments.fields.length).toEqual(1);
      expect(visitor.models.ModelA.fields[1].directives[0].arguments.fields[0]).toEqual('id');
      expect(visitor.models.ModelA.fields[1].directives[0].arguments.indexName).toEqual('byModelA');

      expect(visitor.models.Models).toBeDefined();
      expect(visitor.models.Models.fields.length).toEqual(5);

      const modelA = visitor.models.Models.fields.find(f => f.name === 'modelA');
      expect(modelA).toBeDefined();
      expect(modelA.directives[0].name).toEqual('belongsTo');
      expect(modelA.directives[0].arguments.fields.length).toEqual(1);
      expect(modelA.directives[0].arguments.fields[0]).toEqual('modelAID');

      const modelB = visitor.models.Models.fields.find(f => f.name === 'modelB');
      expect(modelB).toBeDefined();
      expect(modelB.directives[0].name).toEqual('belongsTo');
      expect(modelB.directives[0].arguments.fields.length).toEqual(1);
      expect(modelB.directives[0].arguments.fields[0]).toEqual('modelBID');

      expect(visitor.models.ModelB.fields.length).toEqual(4);
      expect(visitor.models.ModelB.fields[1].directives[0].name).toEqual('hasMany');
      expect(visitor.models.ModelB.fields[1].directives[0].arguments.fields.length).toEqual(1);
      expect(visitor.models.ModelB.fields[1].directives[0].arguments.fields[0]).toEqual('id');
      expect(visitor.models.ModelB.fields[1].directives[0].arguments.indexName).toEqual('byModelB');
    });
  });

  describe('manyToMany with sort key testing', () => {
    const schema = /* GraphQL */ `
      type ModelA @model {
        id: ID! @primaryKey(sortKeyFields: ["sortId"])
        sortId: ID!
        models: [ModelB] @manyToMany(relationName: "ModelAModelB")
      }
      type ModelB @model {
        id: ID! @primaryKey(sortKeyFields: ["sortId"])
        sortId: ID!
        models: [ModelA] @manyToMany(relationName: "ModelAModelB")
      }
    `;
    const { models } = createAndGeneratePipelinedTransformerVisitor(schema);
    const { ModelAModelB } = models;
    it('should generate correct fields and secondary keys for intermediate type', () => {
      expect(ModelAModelB).toBeDefined();
      expect(ModelAModelB.fields.length).toEqual(7);
      const modelASortKeyField = ModelAModelB.fields.find(f => f.name === 'modelAsortId');
      expect(modelASortKeyField).toBeDefined();
      expect(modelASortKeyField.type).toEqual('ID');
      expect(modelASortKeyField.isNullable).toBe(false);
      const modelBSortKeyField = ModelAModelB.fields.find(f => f.name === 'modelBsortId');
      expect(modelBSortKeyField).toBeDefined();
      expect(modelBSortKeyField.type).toEqual('ID');
      expect(modelBSortKeyField.isNullable).toBe(false);
      const modelAIndexDirective = ModelAModelB.directives.find(d => d.name === 'key' && d.arguments.name === 'byModelA');
      expect(modelAIndexDirective).toBeDefined();
      expect(modelAIndexDirective.arguments.fields).toEqual(['modelAID', 'modelAsortId']);
      const modelBIndexDirective = ModelAModelB.directives.find(d => d.name === 'key' && d.arguments.name === 'byModelB');
      expect(modelBIndexDirective).toBeDefined();
      expect(modelBIndexDirective.arguments.fields).toEqual(['modelBID', 'modelBsortId']);
    });
  });

  describe('Graphql V2 fix tests for multiple has many relations of only one model type', () => {
    const schema = /* GraphQL*/ `
      type Registration @model {
        id: ID! @primaryKey
        meetingId: ID @index(name: "byMeeting", sortKeyFields: ["attendeeId"])
        meeting: Meeting! @belongsTo(fields: ["meetingId"])
        attendeeId: ID @index(name: "byAttendee", sortKeyFields: ["meetingId"])
        attendee: Attendee! @belongsTo(fields: ["attendeeId"])
      }
      type Meeting @model {
        id: ID! @primaryKey
        title: String!
        attendees: [Registration] @hasMany(indexName: "byMeeting", fields: ["id"])
      }
      
      type Attendee @model {
        id: ID! @primaryKey
        meetings: [Registration] @hasMany(indexName: "byAttendee", fields: ["id"])
      }
    `;
    it(`should not throw error when processing models`, () => {
      expect(() => createAndGeneratePipelinedTransformerVisitor(schema)).not.toThrow();
    });
  });
});
Example #25
Source File: process.ts    From graphql-mesh with MIT License 4 votes vote down vote up
export async function processConfig(
  config: YamlConfig.Config,
  options?: ConfigProcessOptions
): Promise<ProcessedConfig> {
  if (config.skipSSLValidation) {
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  }

  const importCodes: string[] = [
    `import type { GetMeshOptions } from '@graphql-mesh/runtime';`,
    `import type { YamlConfig } from '@graphql-mesh/types';`,
  ];
  const codes: string[] = [
    `export const rawServeConfig: YamlConfig.Config['serve'] = ${JSON.stringify(config.serve)} as any`,
    `export async function getMeshOptions(): Promise<GetMeshOptions> {`,
  ];

  const {
    dir,
    importFn = defaultImportFn,
    store: providedStore,
    artifactsDir,
    additionalPackagePrefixes,
  } = options || {};

  if (config.require) {
    await Promise.all(config.require.map(mod => importFn(mod)));
    for (const mod of config.require) {
      importCodes.push(`import '${mod}';`);
    }
  }

  const rootStore = providedStore || getDefaultMeshStore(dir, importFn, artifactsDir || '.mesh');

  const {
    pubsub,
    importCode: pubsubImportCode,
    code: pubsubCode,
  } = await resolvePubSub(config.pubsub, importFn, dir, additionalPackagePrefixes);
  importCodes.push(pubsubImportCode);
  codes.push(pubsubCode);

  const {
    cache,
    importCode: cacheImportCode,
    code: cacheCode,
  } = await resolveCache(config.cache, importFn, rootStore, dir, pubsub, additionalPackagePrefixes);
  importCodes.push(cacheImportCode);
  codes.push(cacheCode);

  const sourcesStore = rootStore.child('sources');
  codes.push(`const sourcesStore = rootStore.child('sources');`);

  const {
    logger,
    importCode: loggerImportCode,
    code: loggerCode,
  } = await resolveLogger(config.logger, importFn, dir, additionalPackagePrefixes);
  importCodes.push(loggerImportCode);
  codes.push(loggerCode);

  codes.push(`const sources = [];`);
  codes.push(`const transforms = [];`);
  codes.push(`const additionalEnvelopPlugins = [];`);

  const mergerName = config.merger || (config.sources.length > 1 ? 'stitching' : 'bare');

  const [sources, transforms, additionalEnvelopPlugins, additionalTypeDefs, additionalResolvers, merger, documents] =
    await Promise.all([
      Promise.all(
        config.sources.map<Promise<MeshResolvedSource>>(async (source, sourceIndex) => {
          const handlerName = Object.keys(source.handler)[0].toString();
          const handlerConfig = source.handler[handlerName];
          const handlerVariableName = camelCase(`${source.name}_Handler`);
          const transformsVariableName = camelCase(`${source.name}_Transforms`);
          codes.push(`const ${transformsVariableName} = [];`);
          const [handler, transforms] = await Promise.all([
            await getPackage<MeshHandlerLibrary>({
              name: handlerName,
              type: 'handler',
              importFn,
              cwd: dir,
              additionalPrefixes: additionalPackagePrefixes,
            }).then(({ resolved: HandlerCtor, moduleName }) => {
              if (options.generateCode) {
                const handlerImportName = pascalCase(handlerName + '_Handler');
                importCodes.push(`import ${handlerImportName} from ${JSON.stringify(moduleName)}`);
                codes.push(`const ${handlerVariableName} = new ${handlerImportName}({
              name: ${JSON.stringify(source.name)},
              config: ${JSON.stringify(handlerConfig)},
              baseDir,
              cache,
              pubsub,
              store: sourcesStore.child(${JSON.stringify(source.name)}),
              logger: logger.child(${JSON.stringify(source.name)}),
              importFn
            });`);
              }
              return new HandlerCtor({
                name: source.name,
                config: handlerConfig,
                baseDir: dir,
                cache,
                pubsub,
                store: sourcesStore.child(source.name),
                logger: logger.child(source.name),
                importFn,
              });
            }),
            Promise.all(
              (source.transforms || []).map(async (t, transformIndex) => {
                const transformName = Object.keys(t)[0].toString();
                const transformConfig = t[transformName];
                const { resolved: TransformCtor, moduleName } = await getPackage<MeshTransformLibrary>({
                  name: transformName,
                  type: 'transform',
                  importFn,
                  cwd: dir,
                  additionalPrefixes: additionalPackagePrefixes,
                });

                if (options.generateCode) {
                  const transformImportName = pascalCase(transformName + '_Transform');
                  importCodes.push(`import ${transformImportName} from ${JSON.stringify(moduleName)};`);
                  codes.push(`${transformsVariableName}.push(
                new ${transformImportName}({
                  apiName: ${JSON.stringify(source.name)},
                  config: ${JSON.stringify(transformConfig)},
                  baseDir,
                  cache,
                  pubsub,
                  importFn
                })
              );`);
                }

                return new TransformCtor({
                  apiName: source.name,
                  config: transformConfig,
                  baseDir: dir,
                  cache,
                  pubsub,
                  importFn,
                });
              })
            ),
          ]);

          if (options.generateCode) {
            codes.push(`sources.push({
          name: '${source.name}',
          handler: ${handlerVariableName},
          transforms: ${transformsVariableName}
        })`);
          }

          return {
            name: source.name,
            handler,
            transforms,
          };
        })
      ),
      Promise.all(
        config.transforms?.map(async (t, transformIndex) => {
          const transformName = Object.keys(t)[0].toString();
          const transformConfig = t[transformName];
          const { resolved: TransformLibrary, moduleName } = await getPackage<MeshTransformLibrary>({
            name: transformName,
            type: 'transform',
            importFn,
            cwd: dir,
            additionalPrefixes: additionalPackagePrefixes,
          });

          if (options.generateCode) {
            const transformImportName = pascalCase(transformName + '_Transform');
            importCodes.push(`import ${transformImportName} from ${JSON.stringify(moduleName)};`);

            codes.push(`transforms.push(
          new (${transformImportName} as any)({
            apiName: '',
            config: ${JSON.stringify(transformConfig)},
            baseDir,
            cache,
            pubsub,
            importFn
          })
        )`);
          }
          return new TransformLibrary({
            apiName: '',
            config: transformConfig,
            baseDir: dir,
            cache,
            pubsub,
            importFn,
          });
        }) || []
      ),
      Promise.all(
        config.plugins?.map(async (p, pluginIndex) => {
          const pluginName = Object.keys(p)[0].toString();
          const pluginConfig = p[pluginName];
          if (ENVELOP_CORE_PLUGINS_MAP[pluginName] != null) {
            const { importName, moduleName, pluginFactory } = ENVELOP_CORE_PLUGINS_MAP[pluginName];
            if (options.generateCode) {
              importCodes.push(`import { ${importName} } from ${JSON.stringify(moduleName)};`);
              codes.push(`additionalEnvelopPlugins.push(${importName}(${JSON.stringify(pluginConfig)}))`);
            }
            return pluginFactory(pluginConfig);
          }
          let importName: string;
          const { resolved: possiblePluginFactory, moduleName } = await getPackage<any>({
            name: pluginName,
            type: 'plugin',
            importFn,
            cwd: dir,
            additionalPrefixes: [...additionalPackagePrefixes, '@envelop/'],
          });
          let pluginFactory: MeshPluginFactory<YamlConfig.Plugin[keyof YamlConfig.Plugin]>;
          if (typeof possiblePluginFactory === 'function') {
            pluginFactory = possiblePluginFactory;
            if (options.generateCode) {
              importName = pascalCase('use_' + pluginName);
              importCodes.push(`import ${importName} from ${JSON.stringify(moduleName)};`);
              codes.push(`additionalEnvelopPlugins.push(${importName}({
          ...(${JSON.stringify(pluginConfig)}),
          logger: logger.child(${JSON.stringify(pluginName)}),
        }))`);
            }
          } else {
            Object.keys(possiblePluginFactory).forEach(key => {
              if (key.toString().startsWith('use') && typeof possiblePluginFactory[key] === 'function') {
                pluginFactory = possiblePluginFactory[key];
                if (options.generateCode) {
                  importCodes.push(`import { ${importName} } from ${JSON.stringify(moduleName)};`);
                  codes.push(`additionalEnvelopPlugins.push(${importName}(${JSON.stringify(pluginConfig)}])`);
                }
              }
            });
          }
          return pluginFactory({
            ...pluginConfig,
            logger: logger.child(pluginName),
          });
        }) || []
      ),
      resolveAdditionalTypeDefs(dir, config.additionalTypeDefs).then(additionalTypeDefs => {
        if (options.generateCode) {
          codes.push(
            `const additionalTypeDefs = [${(additionalTypeDefs || []).map(
              parsedTypeDefs => `parse(${JSON.stringify(print(parsedTypeDefs))}),`
            )}] as any[];`
          );
          if (additionalTypeDefs?.length) {
            importCodes.push(`import { parse } from 'graphql';`);
          }
        }
        return additionalTypeDefs;
      }),
      options?.ignoreAdditionalResolvers
        ? []
        : resolveAdditionalResolvers(dir, config.additionalResolvers, importFn, pubsub),
      getPackage<MeshMergerLibrary>({
        name: mergerName,
        type: 'merger',
        importFn,
        cwd: dir,
        additionalPrefixes: additionalPackagePrefixes,
      }).then(({ resolved: Merger, moduleName }) => {
        if (options.generateCode) {
          const mergerImportName = pascalCase(`${mergerName}Merger`);
          importCodes.push(`import ${mergerImportName} from ${JSON.stringify(moduleName)};`);
          codes.push(`const merger = new(${mergerImportName} as any)({
        cache,
        pubsub,
        logger: logger.child('${mergerName}Merger'),
        store: rootStore.child('${mergerName}Merger')
      })`);
        }
        return new Merger({
          cache,
          pubsub,
          logger: logger.child(`${mergerName}Merger`),
          store: rootStore.child(`${mergerName}Merger`),
        });
      }),
      resolveDocuments(config.documents, dir),
    ]);

  if (options.generateCode) {
    if (config.additionalResolvers?.length) {
      importCodes.push(`import { resolveAdditionalResolvers } from '@graphql-mesh/utils';`);

      codes.push(`const additionalResolversRawConfig = [];`);

      for (const additionalResolverDefinitionIndex in config.additionalResolvers) {
        const additionalResolverDefinition = config.additionalResolvers[additionalResolverDefinitionIndex];
        if (typeof additionalResolverDefinition === 'string') {
          importCodes.push(
            `import * as additionalResolvers$${additionalResolverDefinitionIndex} from '${pathModule
              .join('..', additionalResolverDefinition)
              .split('\\')
              .join('/')}';`
          );
          codes.push(
            `additionalResolversRawConfig.push(additionalResolvers$${additionalResolverDefinitionIndex}.resolvers || additionalResolvers$${additionalResolverDefinitionIndex}.default || additionalResolvers$${additionalResolverDefinitionIndex})`
          );
        } else {
          codes.push(`additionalResolversRawConfig.push(${JSON.stringify(additionalResolverDefinition)});`);
        }
      }

      codes.push(`const additionalResolvers = await resolveAdditionalResolvers(
      baseDir,
      additionalResolversRawConfig,
      importFn,
      pubsub
  )`);
    } else {
      codes.push(`const additionalResolvers = [] as any[]`);
    }
  }

  if (config.additionalEnvelopPlugins) {
    importCodes.push(
      `import importedAdditionalEnvelopPlugins from '${pathModule
        .join('..', config.additionalEnvelopPlugins)
        .split('\\')
        .join('/')}';`
    );
    const importedAdditionalEnvelopPlugins = await importFn(
      pathModule.isAbsolute(config.additionalEnvelopPlugins)
        ? config.additionalEnvelopPlugins
        : pathModule.join(dir, config.additionalEnvelopPlugins)
    );
    if (typeof importedAdditionalEnvelopPlugins === 'function') {
      const factoryResult = await importedAdditionalEnvelopPlugins(config);
      if (Array.isArray(factoryResult)) {
        if (options.generateCode) {
          codes.push(`additionalEnvelopPlugins.push(...(await importedAdditionalEnvelopPlugins()));`);
        }
        additionalEnvelopPlugins.push(...factoryResult);
      } else {
        if (options.generateCode) {
          codes.push(`additionalEnvelopPlugins.push(await importedAdditionalEnvelopPlugins());`);
        }
        additionalEnvelopPlugins.push(factoryResult);
      }
    } else {
      if (Array.isArray(importedAdditionalEnvelopPlugins)) {
        if (options.generateCode) {
          codes.push(`additionalEnvelopPlugins.push(...importedAdditionalEnvelopPlugins)`);
        }
        additionalEnvelopPlugins.push(...importedAdditionalEnvelopPlugins);
      } else {
        if (options.generateCode) {
          codes.push(`additionalEnvelopPlugins.push(importedAdditionalEnvelopPlugins)`);
        }
        additionalEnvelopPlugins.push(importedAdditionalEnvelopPlugins);
      }
    }
  }

  if (options.generateCode) {
    importCodes.push(`import { printWithCache } from '@graphql-mesh/utils';`);
    const documentVariableNames: string[] = [];
    if (documents?.length) {
      const allDocumentNodes: DocumentNode = concatAST(
        documents.map(document => document.document || parseWithCache(document.rawSDL))
      );
      visit(allDocumentNodes, {
        OperationDefinition(node) {
          documentVariableNames.push(pascalCase(node.name.value + '_Document'));
        },
      });
    }

    codes.push(`
  return {
    sources,
    transforms,
    additionalTypeDefs,
    additionalResolvers,
    cache,
    pubsub,
    merger,
    logger,
    additionalEnvelopPlugins,
    get documents() {
      return [
      ${documentVariableNames
        .map(
          documentVarName => `{
        document: ${documentVarName},
        get rawSDL() {
          return printWithCache(${documentVarName});
        },
        location: '${documentVarName}.graphql'
      }`
        )
        .join(',')}
    ];
    },
  };
}`);
  }
  return {
    sources,
    transforms,
    additionalTypeDefs,
    additionalResolvers,
    cache,
    merger,
    pubsub,
    config,
    documents,
    logger,
    store: rootStore,
    additionalEnvelopPlugins,
    code: [...new Set([...importCodes, ...codes])].join('\n'),
  };
}
Example #26
Source File: sibling-operations.ts    From graphql-eslint with MIT License 4 votes vote down vote up
export function getSiblingOperations(projectForFile: GraphQLProjectConfig): SiblingOperations {
  const siblings = getSiblings(projectForFile);

  if (siblings.length === 0) {
    let printed = false;

    const noopWarn = () => {
      if (!printed) {
        logger.warn(
          'getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!'
        );
        printed = true;
      }
      return [];
    };

    return {
      available: false,
      getFragment: noopWarn,
      getFragments: noopWarn,
      getFragmentByType: noopWarn,
      getFragmentsInUse: noopWarn,
      getOperation: noopWarn,
      getOperations: noopWarn,
      getOperationByType: noopWarn,
    };
  }

  // Since the siblings array is cached, we can use it as cache key.
  // We should get the same array reference each time we get
  // to this point for the same graphql project
  if (siblingOperationsCache.has(siblings)) {
    return siblingOperationsCache.get(siblings);
  }

  let fragmentsCache: FragmentSource[] | null = null;

  const getFragments = (): FragmentSource[] => {
    if (fragmentsCache === null) {
      const result: FragmentSource[] = [];

      for (const source of siblings) {
        for (const definition of source.document.definitions) {
          if (definition.kind === Kind.FRAGMENT_DEFINITION) {
            result.push({
              filePath: source.location,
              document: definition,
            });
          }
        }
      }
      fragmentsCache = result;
    }
    return fragmentsCache;
  };

  let cachedOperations: OperationSource[] | null = null;

  const getOperations = (): OperationSource[] => {
    if (cachedOperations === null) {
      const result: OperationSource[] = [];

      for (const source of siblings) {
        for (const definition of source.document.definitions) {
          if (definition.kind === Kind.OPERATION_DEFINITION) {
            result.push({
              filePath: source.location,
              document: definition,
            });
          }
        }
      }
      cachedOperations = result;
    }
    return cachedOperations;
  };

  const getFragment = (name: string) => getFragments().filter(f => f.document.name?.value === name);

  const collectFragments = (
    selectable: SelectionSetNode | OperationDefinitionNode | FragmentDefinitionNode,
    recursive,
    collected = new Map<string, FragmentDefinitionNode>()
  ) => {
    visit(selectable, {
      FragmentSpread(spread) {
        const fragmentName = spread.name.value;
        const [fragment] = getFragment(fragmentName);

        if (!fragment) {
          logger.warn(
            `Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`
          );
          return;
        }
        if (!collected.has(fragmentName)) {
          collected.set(fragmentName, fragment.document);
          if (recursive) {
            collectFragments(fragment.document, recursive, collected);
          }
        }
      },
    });
    return collected;
  };

  const siblingOperations: SiblingOperations = {
    available: true,
    getFragment,
    getFragments,
    getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
    getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
    getOperation: name => getOperations().filter(o => o.document.name?.value === name),
    getOperations,
    getOperationByType: type => getOperations().filter(o => o.document.operation === type),
  };

  siblingOperationsCache.set(siblings, siblingOperations);
  return siblingOperations;
}
Example #27
Source File: require-id-when-available.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
  meta: {
    type: 'suggestion',
    // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
    hasSuggestions: true,
    docs: {
      category: 'Operations',
      description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      requiresSchema: true,
      requiresSiblings: true,
      examples: [
        {
          title: 'Incorrect',
          code: /* GraphQL */ `
            # In your schema
            type User {
              id: ID!
              name: String!
            }

            # Query
            query {
              user {
                name
              }
            }
          `,
        },
        {
          title: 'Correct',
          code: /* GraphQL */ `
            # In your schema
            type User {
              id: ID!
              name: String!
            }

            # Query
            query {
              user {
                id
                name
              }
            }

            # Selecting \`id\` with an alias is also valid
            query {
              user {
                id: name
              }
            }
          `,
        },
      ],
      recommended: true,
    },
    messages: {
      [RULE_ID]:
        "Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.",
    },
    schema: {
      definitions: {
        asString: {
          type: 'string',
        },
        asArray: ARRAY_DEFAULT_OPTIONS,
      },
      type: 'array',
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        properties: {
          fieldName: {
            oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
            default: DEFAULT_ID_FIELD_NAME,
          },
        },
      },
    },
  },
  create(context) {
    const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
    const siblings = requireSiblingsOperations(RULE_ID, context);
    const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
    const idNames = asArray(fieldName);

    // Check selections only in OperationDefinition,
    // skip selections of OperationDefinition and InlineFragment
    const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
    const typeInfo = new TypeInfo(schema);

    function checkFragments(node: GraphQLESTreeNode<SelectionSetNode>): void {
      for (const selection of node.selections) {
        if (selection.kind !== Kind.FRAGMENT_SPREAD) {
          continue;
        }

        const [foundSpread] = siblings.getFragment(selection.name.value);
        if (!foundSpread) {
          continue;
        }

        const checkedFragmentSpreads = new Set<string>();
        const visitor = visitWithTypeInfo(typeInfo, {
          SelectionSet(node, key, parent: ASTNode) {
            if (parent.kind === Kind.FRAGMENT_DEFINITION) {
              checkedFragmentSpreads.add(parent.name.value);
            } else if (parent.kind !== Kind.INLINE_FRAGMENT) {
              checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads);
            }
          },
        });

        visit(foundSpread.document, visitor);
      }
    }

    function checkSelections(
      node: OmitRecursively<SelectionSetNode, 'loc'>,
      type: GraphQLOutputType,
      // Fragment can be placed in separate file
      // Provide actual fragment spread location instead of location in fragment
      loc: ESTree.Position,
      // Can't access to node.parent in GraphQL AST.Node, so pass as argument
      parent: any,
      checkedFragmentSpreads = new Set<string>()
    ): void {
      const rawType = getBaseType(type);
      const isObjectType = rawType instanceof GraphQLObjectType;
      const isInterfaceType = rawType instanceof GraphQLInterfaceType;

      if (!isObjectType && !isInterfaceType) {
        return;
      }
      const fields = rawType.getFields();
      const hasIdFieldInType = idNames.some(name => fields[name]);

      if (!hasIdFieldInType) {
        return;
      }

      function hasIdField({ selections }: typeof node): boolean {
        return selections.some(selection => {
          if (selection.kind === Kind.FIELD) {
            if (selection.alias && idNames.includes(selection.alias.value)) {
              return true;
            }

            return idNames.includes(selection.name.value);
          }

          if (selection.kind === Kind.INLINE_FRAGMENT) {
            return hasIdField(selection.selectionSet);
          }

          if (selection.kind === Kind.FRAGMENT_SPREAD) {
            const [foundSpread] = siblings.getFragment(selection.name.value);
            if (foundSpread) {
              const fragmentSpread = foundSpread.document;
              checkedFragmentSpreads.add(fragmentSpread.name.value);
              return hasIdField(fragmentSpread.selectionSet);
            }
          }
          return false;
        });
      }

      const hasId = hasIdField(node);

      checkFragments(node as GraphQLESTreeNode<SelectionSetNode>);

      if (hasId) {
        return;
      }

      const pluralSuffix = idNames.length > 1 ? 's' : '';
      const fieldName = englishJoinWords(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``));

      const addition =
        checkedFragmentSpreads.size === 0
          ? ''
          : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${englishJoinWords(
              [...checkedFragmentSpreads].map(name => `\`${name}\``)
            )}`;

      const problem: ReportDescriptor = {
        loc,
        messageId: RULE_ID,
        data: {
          pluralSuffix,
          fieldName,
          addition,
        },
      };

      // Don't provide suggestions for selections in fragments as fragment can be in a separate file
      if ('type' in node) {
        problem.suggest = idNames.map(idName => ({
          desc: `Add \`${idName}\` selection`,
          fix: fixer => fixer.insertTextBefore((node as any).selections[0], `${idName} `),
        }));
      }
      context.report(problem);
    }

    return {
      [selector](node: GraphQLESTreeNode<SelectionSetNode, true>) {
        const typeInfo = node.typeInfo();
        if (typeInfo.gqlType) {
          checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
        }
      },
    };
  },
}
Example #28
Source File: graphql-js-validation.ts    From graphql-eslint with MIT License 4 votes vote down vote up
GRAPHQL_JS_VALIDATIONS: Record<string, GraphQLESLintRule> = Object.assign(
  {},
  validationToRule('executable-definitions', 'ExecutableDefinitions', {
    category: 'Operations',
    description:
      'A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.',
    requiresSchema: true,
  }),
  validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
    category: 'Operations',
    description:
      'A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as `__typename`.',
    requiresSchema: true,
  }),
  validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
    category: 'Operations',
    description:
      'Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.',
    requiresSchema: true,
  }),
  validationToRule('known-argument-names', 'KnownArgumentNames', {
    category: ['Schema', 'Operations'],
    description: 'A GraphQL field is only valid if all supplied arguments are defined by that field.',
    requiresSchema: true,
  }),
  validationToRule(
    'known-directives',
    'KnownDirectives',
    {
      category: ['Schema', 'Operations'],
      description:
        'A GraphQL document is only valid if all `@directive`s are known by the schema and legally positioned.',
      requiresSchema: true,
      examples: [
        {
          title: 'Valid',
          usage: [{ ignoreClientDirectives: ['client'] }],
          code: /* GraphQL */ `
            {
              product {
                someClientField @client
              }
            }
          `,
        },
      ],
    },
    ({ context, node: documentNode }) => {
      const { ignoreClientDirectives = [] } = context.options[0] || {};
      if (ignoreClientDirectives.length === 0) {
        return documentNode;
      }

      const filterDirectives = (node: { directives?: ReadonlyArray<DirectiveNode> }) => ({
        ...node,
        directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
      });

      return visit(documentNode, {
        Field: filterDirectives,
        OperationDefinition: filterDirectives,
      });
    },
    {
      type: 'array',
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['ignoreClientDirectives'],
        properties: {
          ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
        },
      },
    }
  ),
  validationToRule(
    'known-fragment-names',
    'KnownFragmentNames',
    {
      category: 'Operations',
      description:
        'A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.',
      requiresSchema: true,
      requiresSiblings: true,
      examples: [
        {
          title: 'Incorrect',
          code: /* GraphQL */ `
            query {
              user {
                id
                ...UserFields # fragment not defined in the document
              }
            }
          `,
        },
        {
          title: 'Correct',
          code: /* GraphQL */ `
            fragment UserFields on User {
              firstName
              lastName
            }

            query {
              user {
                id
                ...UserFields
              }
            }
          `,
        },
        {
          title: 'Correct (`UserFields` fragment located in a separate file)',
          code: /* GraphQL */ `
            # user.gql
            query {
              user {
                id
                ...UserFields
              }
            }

            # user-fields.gql
            fragment UserFields on User {
              id
            }
          `,
        },
      ],
    },
    handleMissingFragments
  ),
  validationToRule('known-type-names', 'KnownTypeNames', {
    category: ['Schema', 'Operations'],
    description:
      'A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.',
    requiresSchema: true,
  }),
  validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
    category: 'Operations',
    description:
      'A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.',
    requiresSchema: true,
  }),
  validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
    category: 'Schema',
    description: 'A GraphQL document is only valid if it contains only one schema definition.',
  }),
  validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
    category: 'Operations',
    description: 'A GraphQL fragment is only valid when it does not have cycles in fragments usage.',
    requiresSchema: true,
  }),
  validationToRule(
    'no-undefined-variables',
    'NoUndefinedVariables',
    {
      category: 'Operations',
      description:
        'A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.',
      requiresSchema: true,
      requiresSiblings: true,
    },
    handleMissingFragments
  ),
  validationToRule(
    'no-unused-fragments',
    'NoUnusedFragments',
    {
      category: 'Operations',
      description:
        'A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.',
      requiresSchema: true,
      requiresSiblings: true,
    },
    ({ ruleId, context, node }) => {
      const siblings = requireSiblingsOperations(ruleId, context);
      const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce<
        Record<string, ExecutableDefinitionNode[]>
      >((map, { filePath, document }) => {
        map[filePath] ??= [];
        map[filePath].push(document);
        return map;
      }, Object.create(null));

      const getParentNode = (currentFilePath: string, node: DocumentNode): DocumentNode => {
        const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node);
        if (fragmentDefs.size === 0) {
          return node;
        }
        // skip iteration over documents for current filepath
        delete FilePathToDocumentsMap[currentFilePath];

        for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
          const missingFragments = getMissingFragments({
            kind: Kind.DOCUMENT,
            definitions: documents,
          });
          const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));

          if (isCurrentFileImportFragment) {
            return getParentNode(filePath, {
              kind: Kind.DOCUMENT,
              definitions: [...node.definitions, ...documents],
            });
          }
        }
        return node;
      };

      return getParentNode(context.getFilename(), node);
    }
  ),
  validationToRule(
    'no-unused-variables',
    'NoUnusedVariables',
    {
      category: 'Operations',
      description:
        'A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.',
      requiresSchema: true,
      requiresSiblings: true,
    },
    handleMissingFragments
  ),
  validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
    category: 'Operations',
    description:
      'A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.',
    requiresSchema: true,
  }),
  validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
    category: 'Operations',
    description:
      'A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.',
    requiresSchema: true,
  }),
  validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
    category: 'Schema',
    description: 'A type extension is only valid if the type is defined and has the same kind.',
    // TODO: add in graphql-eslint v4
    recommended: false,
    requiresSchema: true,
    isDisabledForAllConfig: true,
  }),
  validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
    category: ['Schema', 'Operations'],
    description:
      'A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.',
    requiresSchema: true,
  }),
  validationToRule('scalar-leafs', 'ScalarLeafs', {
    category: 'Operations',
    description:
      'A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.',
    requiresSchema: true,
  }),
  validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
    category: 'Operations',
    description: 'A GraphQL subscription is valid only if it contains a single root field.',
    requiresSchema: true,
  }),
  validationToRule('unique-argument-names', 'UniqueArgumentNames', {
    category: 'Operations',
    description: 'A GraphQL field or directive is only valid if all supplied arguments are uniquely named.',
    requiresSchema: true,
  }),
  validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
    category: 'Schema',
    description: 'A GraphQL document is only valid if all defined directives have unique names.',
  }),
  validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
    category: ['Schema', 'Operations'],
    description:
      'A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.',
    requiresSchema: true,
  }),
  validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
    category: 'Schema',
    description: 'A GraphQL enum type is only valid if all its values are uniquely named.',
    recommended: false,
    isDisabledForAllConfig: true,
  }),
  validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
    category: 'Schema',
    description: 'A GraphQL complex type is only valid if all its fields are uniquely named.',
  }),
  validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
    category: 'Operations',
    description: 'A GraphQL input object value is only valid if all supplied fields are uniquely named.',
  }),
  validationToRule('unique-operation-types', 'UniqueOperationTypes', {
    category: 'Schema',
    description: 'A GraphQL document is only valid if it has only one type per operation.',
  }),
  validationToRule('unique-type-names', 'UniqueTypeNames', {
    category: 'Schema',
    description: 'A GraphQL document is only valid if all defined types have unique names.',
  }),
  validationToRule('unique-variable-names', 'UniqueVariableNames', {
    category: 'Operations',
    description: 'A GraphQL operation is only valid if all its variables are uniquely named.',
    requiresSchema: true,
  }),
  validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
    category: 'Operations',
    description: 'A GraphQL document is only valid if all value literals are of the type expected at their position.',
    requiresSchema: true,
  }),
  validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
    category: 'Operations',
    description:
      'A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).',
    requiresSchema: true,
  }),
  validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
    category: 'Operations',
    description: 'Variables passed to field arguments conform to type.',
    requiresSchema: true,
  })
)