{"id":18982284,"url":"https://github.com/profusion/apollo-validation-directives","last_synced_at":"2025-04-13T08:17:18.627Z","repository":{"id":38427104,"uuid":"242187476","full_name":"profusion/apollo-validation-directives","owner":"profusion","description":"GraphQL directives to implement field validations in Apollo Server","archived":false,"fork":false,"pushed_at":"2023-12-05T17:38:53.000Z","size":675,"stargazers_count":73,"open_issues_count":9,"forks_count":7,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-13T08:16:37.981Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/profusion.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-21T16:51:18.000Z","updated_at":"2023-12-08T12:21:37.000Z","dependencies_parsed_at":"2023-02-18T07:15:53.641Z","dependency_job_id":"3f322875-9958-4da2-af71-e31789e2f212","html_url":"https://github.com/profusion/apollo-validation-directives","commit_stats":{"total_commits":106,"total_committers":11,"mean_commits":9.636363636363637,"dds":0.7547169811320755,"last_synced_commit":"21f22d7847e028f67b378dc3c89494bdfcc7d150"},"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/profusion%2Fapollo-validation-directives","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/profusion%2Fapollo-validation-directives/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/profusion%2Fapollo-validation-directives/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/profusion%2Fapollo-validation-directives/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/profusion","download_url":"https://codeload.github.com/profusion/apollo-validation-directives/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248681519,"owners_count":21144703,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T16:13:02.318Z","updated_at":"2025-04-13T08:17:18.561Z","avatar_url":"https://github.com/profusion.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Validation Directives for Apollo Server\n\nThis project provides useful validation directives to be used with\nthe Apollo Server.\n\nThese are useful to both document the schema, making it clear what is\nexpected, and ease the resolver implementation since it will only be\ncalled after pre-conditions are met.\n# Installation\n```sh\n$ npm i @profusion/apollo-validation-directives\n\n$ yarn add @profusion/apollo-validation-directives\n```\n\n# Helpers\n\nThis project exposes few helpers:\n\n- `EasyDirectiveVisitor` abstract class enables `getTypeDefs()` and\n  `getDirectiveDeclaration()` based on static (class) attributes.\n- `ValidateDirectiveVisitor` builds on top of `EasyDirectiveVisitor`\n  and does all the required work to validate both output and input\n  (arguments). All it needs is `getValidationForArgs()`.\n- `createSchemaMapperForVisitor()` is a helper to adapt the old visitor API\n  to the new one using schema mappers. It takes the directive name and the visitor's\n  instance as arguments.\n- `applyDirectivesToSchema` is useful to apply several directive mappers to a schema.\n  It takes a list of directives and a schema as arguments.\n\n# Directives\n\n## Applying to schema\n\nEach directive instance has an `applyToSchema` method that takes a schema as argument\nand applies the directive to it. Then returns the modified schema.\n\n```typescript\nimport { range as RangeDirective } from '@profusion/apollo-validation-directives';\n\nconst range = new RangeDirective();\nconst schema = range.applyToSchema(\n  makeExecutableSchema(/* ... */),\n);\n```\n\nTo apply several directives easily, use the `applyDirectivesToSchema` helper:\n\n```typescript\nimport {\n  applyDirectivesToSchema,\n  auth,\n  range,\n  stringLength,\n} from '@profusion/apollo-validation-directives';\n\nconst schema = applyDirectivesToSchema(\n  [auth, range, stringLength],\n  makeExecutableSchema(/* ... */),\n);\n```\n\n### Execution order\n\nDue to how the schema mappers works, the directives will be called in the\norder they are mapped to the schema, no matter the order they are put in the schema declaration.\n\nSo be careful with the order you pass the directives to the `applyDirectivesToSchema` helper.\n\nFor example, if you have the schema below:\n\n```graphql\ntype Query {\n  test: (arg: String @trim @stringLength(max: 5)) String\n}\n```\n\nAnd you call `applyDirectivesToSchema([stringLength, trim], schema)`,\nthe `stringLength` directive will be called before the `trim` directive.\n\nRemember to pass the directives to the mapper in the order you want them to be called!\n\n## Access Control\n\nSee [examples/access-control-directives.ts](./examples/access-control-directives.ts)\n\n### `@auth`\n\nThe `@auth` uses the context-provided `isAuthenticated()` function\nand throws `AuthenticationError` if that returns `false`.\n\nIt can be used on each field or on an object type, in this case all\nfields will be marked as authenticated.\n\nThe context must provide `isAuthenticated()` and this function will\nbe called with the resolver arguments. The field resolver is only\ncalled if `isAuthenticated()` returns `true`.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  authenticatedField: Int @auth\n  publicField: String\n}\n\ntype MyAuthenticatedObject @auth {\n  authenticatedField: Int # behaves as @auth\n  anotherAuthenticatedField: String # behaves as @auth\n}\n```\n\nCode:\n\n```typescript\nimport { ApolloServer } from '@apollo/server';\nimport { startStandaloneServer } from '@apollo/server/standalone';\nimport { makeExecutableSchema } from '@graphql-tools/schema';\nimport { auth, applyDirectivesToSchema } from '@profusion/apollo-validation-directives';\n\nconst schema = applyDirectivesToSchema(\n  [auth],\n  makeExecutableSchema({\n    resolvers,\n    typeDefs: [\n      ...auth.getTypeDefs(),\n      ...yourTypeDefs,\n    ],\n  })\n)\n\nconst server = new ApolloServer({ schema });\n\nstartStandaloneServer(server, {\n  context: async (expressContext) =\u003e {\n    const { authorization } = expressContext.req.headers;\n    const isAuthenticated = isAuthorizationValid(authorization);\n    return auth.createDirectiveContext({\n      isAuthenticated: () =\u003e isAuthenticated,\n    });\n  },\n})\n```\n\n### `@hasPermissions()`\n\nThe `@hasPermissions()` uses the context-provided\n`checkMissingPermissions()` to see if the current request contains the\nrequired permissions. If the `policy: THROW` (default) is used, then it\nwill throw `ForbiddenError`. If the `policy: RESOLVER` is used, the\ncheck is done inside the resolver and the `GraphQLResolveInfo` argument will receive an extra\nfield `missingPermissions: string[]` that is undefined if nothing is missing,\notherwise it contains the missing permissions. For instance it may\nallow the execution with some restrictions, such as mask values, filter\nand only return the owned fields, etc.\n\nIt can be used on each field or on an object type, in this case all\nfields will be marked as requiring the same permissions, with the\nsame policy.\n\nThe context must provide `checkMissingPermissions()` and this function\nwill be called with the list of required `permissions` and a `cacheKey`\nfollowed by the resolver arguments.\n\nThe `cacheKey` uniquely identifies the list of `permission` (ie:\n`JSON.stringify(Array.from(permissions).sort())`) and may be used\nto speed up recurrent checks.\n\nIf `policy: THROW` (default) the field resolver is only called if\n`checkMissingPermissions()` returns `null` or an empty list.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  onlyAllowedMayRead: Int @hasPermissions(permissions: [\"x\", \"y\"])\n  email: String @hasPermissions(\n    permissions: [\"email:read\"],\n    policy: RESOLVER # example: mask emails if permission is not granted\n  )\n  publicField: String\n}\n\ntype MyRestrictedObject @hasPermissions(permissions: [\"x\"]) {\n  restrictedField: Int # behaves as @hasPermissions(permissions: [\"x\"])\n  anotherRestrictedField: String # behaves as @hasPermissions(permissions: [\"x\"])\n}\n```\n\nCode:\n\n```typescript\nimport { ApolloServer } from '@apollo/server';\nimport { startStandaloneServer } from '@apollo/server/standalone';\nimport { makeExecutableSchema } from '@graphql-tools/schema';\nimport {\n  hasPermissions,\n  applyDirectivesToSchema\n} from '@profusion/apollo-validation-directives';\n\nconst schema = applyDirectivesToSchema(\n  [hasPermissions],\n  makeExecutableSchema({\n    resolvers,\n    typeDefs: [\n      ...yourTypeDefs,\n      ...hasPermissions.getTypeDefs(),\n    ],\n  })\n)\n\nconst server = new ApolloServer({ schema });\n\nstartStandaloneServer(server, {\n  context: async (expressContext) =\u003e {\n    const { authorization } = expressContext.req.headers;\n    return hasPermissions.createDirectiveContext({\n      grantedPermissions: getPermissions(authorization),\n    });\n  },\n})\n```\n\nThere are some cases where the checkMissingPermissions() function is not called, the cases are:\n\n  - `@hasPermissions` used on `InputObject`:\n    - No fields were sent;\n    - All fields of the received object are equal to their default values;\n\n  - `@hasPermissions` used on `InputFieldDefinition`:\n    - If the field is an `InputObject` (or list of):\n        - Same as `InputObject`;\n    - If the field is a `Scalar` (or list of):\n        - No value was sent in the annotated field;\n        - Value received in the field is equal to the default value;\n\n  - `@hasPermissions` used on `ArgumentDefinition`:\n    - Same as `InputFieldDefinition`;\n\n## Value Validation\n\nThe value validation directives do not require a specific context.\n\nThey can all be used on multiple locations: `ARGUMENT_DEFINITION`,\n`FIELD_DEFINITION`, `INPUT_FIELD_DEFINITION`, `INPUT_OBJECT` and\n`OBJECT`. When used in field containers (`INPUT_OBJECT` or `OBJECT`)\nall fields get the same validation.\n\nValidated fields will get an extra property `validationErrors` which\nis present in `GraphQLResolveInfo` of type\n`[ValidatedInputError!]`. It will be injected by resolver wrapper\nand will be `null` if no errors or will contain a non-empty list of\nerrors that were captured, in this case the nullable fields are\nconverted into `null`, similar to what is done for failed resolver\nfields. These types are exposed by\n`ValidateDirectiveVisitor.getMissingCommonTypeDefs()`.\n\n```typescript\nimport { ApolloServer } from '@apollo/server';\nimport { startStandaloneServer } from '@apollo/server/standalone';\nimport { makeExecutableSchema } from '@graphql-tools/schema';\nimport {\n  range,\n  ValidateDirectiveVisitor,\n  applyDirectivesToSchema,\n} from '@profusion/apollo-validation-directives';\n\nconst schema = applyDirectivesToSchema(\n  [range],\n  makeExecutableSchema({\n    typeDefs: [\n      ...yourTypeDefs,\n      ...ValidateDirectiveVisitor.getMissingCommonTypeDefs(),\n      ...range.getTypeDefs(),\n      // ... any other validation here ...\n    ],\n    resolvers,\n  })\n);\n\nconst server = new ApolloServer({ schema });\n```\n\nSee [examples/value-validation-directives.ts](./examples/value-validation-directives.ts)\n\n### `@range()`\n\nThe `@range()` limits a number between minimum and maximum values. If\nany of `min: null` (or not specified), then there is no minimum\nboundary. Likewise, if `max: null` (or not specified), there is no\nmaximum boundary. If both are `null` the directive has no effect.\n\nThe boundary values are included in the allowed numbers, that is:\n`min \u003c= value \u0026\u0026 value \u003c= max`.\n\nIf the value is out of boundaries, it will throw `ValidationError()`.\n\nIt can be used on each field or on an object/input type, in this case\nall fields will be marked with the same range.\n\nIf used on lists, it will apply to each item.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  limitedInt: Int! @range(min: 0, max: 100)\n  worksWithNullable: Int @range(min: 0, max: 100)\n  positiveIntegers: Int! @range(min: 1)\n  negativeIntegers: Int! @range(max: -1)\n  unlimited: Int! @range\n  limitedFloat: Float! @range(min: -0.5, max: 0.5)\n  onlyNumbersAreHandled: String @range(min: 0, max: 10) # unlimited/ignored\n}\n\ninput SomeInput {\n  limitedInt: Int! @range(min: 0, max: 100)\n}\n```\n\n### `@listLength()`\n\nThe `@listLength()` limits a list between minimum and maximum\nlength. If any of `min: null` (or not specified), then there is no\nminimum boundary. Likewise, if `max: null` (or not specified), there is\nno maximum boundary. If both are `null` the directive has no effect.\n\nThe boundary values are included in the allowed numbers, that is:\n`min \u003c= length \u0026\u0026 length \u003c= max`.\n\nIf the list length is out of boundaries, it will throw\n`ValidationError()`.\n\nIt can be used on each field or on an object/input type, in this case\nall fields will be marked with the same list length.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  limitedArray: [String!]! @listLength(min: 1, max: 5)\n}\n\ninput SomeInput {\n  limitedArray: [String!]! @listLength(min: 1, max: 5)\n}\n```\n\n### `@stringLength()`\n\nThe `@stringLength()` limits a string between minimum and maximum\nlength. If any of `min: null` (or not specified), then there is no\nminimum boundary. Likewise, if `max: null` (or not specified), there is\nno maximum boundary. If both are `null` the directive has no effect.\n\nThe boundary values are included in the allowed numbers, that is:\n`min \u003c= length \u0026\u0026 length \u003c= max`.\n\nIf the string length is out of boundaries, it will throw\n`ValidationError()`.\n\nIt can be used on each field or on an object/input type, in this case\nall fields will be marked with the same string length.\n\nIf used on lists, it will apply to each item.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  limitedString: String! @stringLength(min: 1, max: 100)\n  worksWithNullable: String @stringLength(min: 1, max: 100)\n  atLeast1Char: String! @stringLength(min: 1)\n  atMost10Chars: String! @stringLength(max: 10)\n  unlimited: String! @stringLength()\n  limitedArray: [String!]! @stringLength(min: 1, max: 5)\n}\n\ninput SomeInput {\n  limitedString: String! @stringLength(min: 1, max: 100)\n  limitedArray: [String!]! @stringLength(min: 1, max: 100)\n}\n```\n\n### `@pattern()`\n\nThe `@pattern()` limits a string to match the given regular expression,\notherwise it will throw `ValidationError()`.\n\nIt can be used on each field or on an object/input type, in this case\nall fields will be marked with the same pattern.\n\nIf used on lists, it will apply to each item.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  example: String! @pattern(regexp: \"[A-Za-z]+\")\n  worksWithNullable: String @pattern(regexp: \"[A-Za-z]+\")\n  flagsAreSupported: String! @pattern(regexp: \"[a-z]+\", flags: \"i\")\n}\n\ninput SomeInput {\n  example: String! @pattern(regexp: \"[A-Za-z]+\")\n  worksWithNullable: String @pattern(regexp: \"[A-Za-z]+\")\n  flagsAreSupported: String! @pattern(regexp: \"[a-z]+\", flags: \"i\")\n}\n```\n\n### Relay (Global) Node ID Support\n\nThis package exposes two directives to convert IDs encode and decode Relay's\n[Node](https://facebook.github.io/relay/graphql/objectidentification.htm) interface.\nFor instance, this plays well with https://github.com/profusion/apollo-federation-node-gateway\nthat will collect all the types implementing the Node interface as integers\nso the encoded id is both small and avoid leaking internal details.\n\n### `@selfNodeId`\n\nThe `@selfNodeId` uses the context-provided `toNodeId()` function\nand throws `ValidationError` if that returns `null` and can be\nused to encode an ID to a global Node ID.\n\nIt can be used on any field String or an ID field and\nthe typename used to encode will be the type which\nthe field belongs to. It can be also used in an object,\nwhich will this case automatically annotate the `id` field.\n\nThe context must provide `toNodeId()` and this function will\nbe called with the following arguments. This function receives two\narguments, which are:\n * typename: A string which contains the typename\n * id: The ID itself\nAfter this function executes, it should return an encoded node ID.\n\nGraphQL schema usage:\n\n```gql\ntype SomeObject {\n  id: ID! @selfNodeId\n}\n\ntype MyAuthenticatedObject @selfNodeId {\n  id: ID! # This field will be wrapped and a global node ID will be returned\n}\n```\n\nCode:\n\n```typescript\nimport { ApolloServer } from '@apollo/server';\nimport { startStandaloneServer } from '@apollo/server/standalone';\nimport { makeExecutableSchema } from '@graphql-tools/schema';\nimport {\n  selfNodeId,\n  applyDirectivesToSchema\n} from '@profusion/apollo-validation-directives';\n\nconst schema = applyDirectivesToSchema(\n  [selfNodeId],\n  makeExecutableSchema({\n      resolvers,\n      typeDefs: [\n        ...yourTypeDefs,\n        ...selfNodeId.getTypeDefs(),\n      ],\n  })\n);\n\nconst server = new ApolloServer({ schema });\n\nstartStandaloneServer(server, {\n  context: () =\u003e {\n    return selfNodeId.createDirectiveContext({\n      toNodeId: (typename, id) =\u003e Buffer.from(`${typename}:${id}`).toString('base64'),\n    });\n  },\n})\n```\n\n### `@foreignNodeId`\n\nThe `@foreignNodeId` can be used to and can be\nused to decode a global Node ID to an ID.\nIt uses the context-provided `fromNodeId()` function\nand throws `ValidationError` if that returns `null`, otherwise\nit should return a object with the following interface:\n\n```typescript\ninterface FromNodeIdReturnType {\n  typename: string; // The typename for the decoded ID\n  id: string; // The decoded ID\n}\n```\n\nIn case the returned `typename` does not match the one\nprovided via args to the `@foreignNodeId` directive a\n`ValidationError` will be thrown.\n\nThis directive has an required argument called `typename`\nwhich will be used to validate if the `fromNodeId()` function\ndecoded the global Node ID correctly.\n\nThis directive can be used on query/mutation arguments\nor in input field definitions which matches the ID/string type.\n\nThe context must provide `fromNodeId()` and this function will\nbe called the encoded node id as returned by `toNodeId()` from the\n`@selfNodeId` directive.\n\nGraphQL schema usage:\n\n```gql\ninput InputType {\n  myId: ID! @foreignNodeId(typename: \"X\")\n  otherId: ID! @foreignNodeId(typename: \"Y\")\n  yetAnotherId: ID! @foreignNodeId(typename: \"Z\")\n}\n\ntype Query {\n  work(input: InputType!, id: ID! @foreignNodeId(typename: \"I\"))\n}\n```\n\nCode:\n\n```typescript\nimport { ApolloServer } from '@apollo/server';\nimport { startStandaloneServer } from '@apollo/server/standalone';\nimport { makeExecutableSchema } from '@graphql-tools/schema';\nimport {\n  applyDirectivesToSchema,\n  foreignNodeId,\n} from '@profusion/apollo-validation-directives';\n\nconst schema = applyDirectivesToSchema(\n  [foreignNodeId],\n  makeExecutableSchema({\n    resolvers,\n    typeDefs: [\n      ...yourTypeDefs,\n      ...foreignNodeId.getTypeDefs(),\n    ],\n  })\n);\n\nconst server = new ApolloServer({ schema });\n\nstartStandaloneServer(server, {\n  context: () =\u003e {\n    return foreignNodeId.createDirectiveContext({\n      fromNodeId: (id) =\u003e {\n        const r = Buffer.from(id, 'base64')\n          .toString('ascii')\n          .split(':');\n        return {\n          id: r[1],\n          typename: r[0],\n        };\n      },\n    });\n  },\n});\n```\n\n### Apollo Federation\n\nYou can use this library in your federation subgraphs as well.\n\nIn the example below, we add `@range` and `@stringLength` to the server. You can see a full code example at [examples/federation.ts](./examples/federation.ts).\n\n```typescript\nimport { ApolloServer } from '@apollo/server';\nimport { startStandaloneServer } from '@apollo/server/dist/esm/standalone';\nimport { buildSubgraphSchema } from '@apollo/subgraph';\nimport { gql } from 'graphql-tag';\nimport type { GraphQLResolverMap } from '@apollo/subgraph/dist/schema-helper';\nimport type { DocumentNode, GraphQLSchema } from 'graphql';\n\nimport {\n  ValidateDirectiveVisitor,\n  range,\n  stringLength,\n  applyDirectivesToSchema,\n} from '@profusion/apollo-validation-directives';\n\n// buildSchema.ts\n\nconst buildSchema = (\n  resolvers: GraphQLResolverMap\u003c{}\u003e,\n  typeDefs: DocumentNode,\n): GraphQLSchema =\u003e {\n  const finalTypeDefs = [\n    typeDefs,\n    ...ValidateDirectiveVisitor.getMissingCommonTypeDefs(),\n    ...directives.reduce\u003cDocumentNode[]\u003e(\n      (acc, d) =\u003e acc.concat(d.getTypeDefs()),\n      [],\n    ),\n  ];\n  return buildSubgraphSchema({\n    resolvers: resolvers as GraphQLResolverMap\u003cunknown\u003e,\n    typeDefs: finalTypeDefs,\n  });\n};\n\n// server.ts\n\nconst resolvers = {\n  /*  the resolvers... */\n};\nconst typeDefs = gql`....`;\n\nconst directives = [range, stringLength];\n\nconst server = new ApolloServer({\n  // From buildSchema.ts\n  schema: applyDirectivesToSchema(\n    directives,\n    buildSchema(resolvers, typeDefs),\n  ),\n});\nstartStandaloneServer(server).then(({ url }) =\u003e\n  console.log(`🚀 server ready at ${url}`),\n);\n\n```\n\nSee [examples/federation.ts](./examples/federation.ts)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprofusion%2Fapollo-validation-directives","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprofusion%2Fapollo-validation-directives","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprofusion%2Fapollo-validation-directives/lists"}