{"id":20147258,"url":"https://github.com/voodooattack/metanoia","last_synced_at":"2025-04-09T19:35:38.647Z","repository":{"id":57295739,"uuid":"141180691","full_name":"voodooattack/metanoia","owner":"voodooattack","description":"A set of TypeScript decorators for defining a GraphQL schema directly out of your TypeScript class definitions.","archived":false,"fork":false,"pushed_at":"2018-12-18T12:50:50.000Z","size":310,"stargazers_count":25,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-06T15:55:38.424Z","etag":null,"topics":["decorators","graphql","javascript","typescript","typescript-library"],"latest_commit_sha":null,"homepage":"https://voodooattack.github.io/metanoia/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/voodooattack.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-07-16T18:54:50.000Z","updated_at":"2024-04-02T14:59:54.000Z","dependencies_parsed_at":"2022-08-31T03:01:43.134Z","dependency_job_id":null,"html_url":"https://github.com/voodooattack/metanoia","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voodooattack%2Fmetanoia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voodooattack%2Fmetanoia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voodooattack%2Fmetanoia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voodooattack%2Fmetanoia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/voodooattack","download_url":"https://codeload.github.com/voodooattack/metanoia/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248098325,"owners_count":21047414,"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":["decorators","graphql","javascript","typescript","typescript-library"],"created_at":"2024-11-13T22:28:14.408Z","updated_at":"2025-04-09T19:35:38.616Z","avatar_url":"https://github.com/voodooattack.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Metanoia\n\n\n[![npm](https://img.shields.io/npm/v/metanoia.svg)](https://www.npmjs.com/package/metanoia) \n [![GitHub license](https://img.shields.io/github/license/voodooattack/metanoia.svg)](https://github.com/voodooattack/metanoia/blob/master/LICENSE)\n [![GitHub issues](https://img.shields.io/github/issues/voodooattack/metanoia.svg)](https://github.com/voodooattack/metanoia/issues) \n [![Build Status](https://travis-ci.org/voodooattack/metanoia.svg?branch=master)](https://travis-ci.org/voodooattack/metanoia) [![Coverage Status](https://coveralls.io/repos/github/voodooattack/metanoia/badge.svg)](https://coveralls.io/github/voodooattack/metanoia)\n ![npm type definitions](https://img.shields.io/npm/types/metanoia.svg)\n\nA set of TypeScript decorators for defining a GraphQL schema directly out of your TypeScript class definitions.\n\n### Installation\n\n`npm install metanoia`\n\n### Usage\n\nFirst, let's define a GraphQL interface:\n\n```ts\n\nimport { SchemaBuilder } from 'metanoia';\n\n// Create a new Schema builder, this is the centrepiece of this library.\nconst builder = new SchemaBuilder();\n\n@builder.interfaceType({ resolveType: (value: Node) =\u003e value.kind })\nexport class Node {\n  /**\n   * The most basic attribute identifying a node: its type (class name).\n   * @type {string}\n   */\n  @builder.field(GraphQLString) // define as a `String` field\n  @builder.nonNull() // define as not nullable\n  // attach a description, this will appear in your schema definition\n  @builder.description('Type of this Node.')\n  kind: string = this.constructor.name;\n\n  /**\n   * The unique ID of this node.\n   * @type {string}\n   */\n  @builder.nonNull() // this can not be null!\n  @builder.field(GraphQLID) // define the field with a type of ID\n  @builder.description('A unique ID for this object.')\n  id: string|number;\n}\n\n```\n\nNow we that we've defined our interface, we can define a type that implements it!\n\n```ts\n\n// Define an enum for user roles.\nexport enum UserRoleEnum {\n  administrator = 'administrator',\n  moderator = 'moderator',\n  subscriber = 'subscriber'\n}\n\n// We have to use this method to decorate it, since TypeScript does\n// not allow decorators on enums yet.\nbuilder.decorateEnum(UserRoleEnum, {\n  name: 'UserRole', // The name to use in the GraphQL schema.\n  description: 'A user\\'s role.',\n  // Describe individual values here.\n  // Anything not included here is not part of the schema!\n  values: {\n    subscriber: { description: 'A subscriber.' },\n    moderator: { description: 'A moderator.' },\n    administrator: { description: 'An administrator.' },\n  }\n});\n\n@builder.type({\n  description: 'A user.',\n  // list of interfaces to inherit,\n  // note that `User` will actually inherit all the fields from `Node`\n  // through TypeScript inheritance\n  interfaces: () =\u003e [Node]\n})\nexport class User extends Node {\n\n  // Define a `role` field of our enum type, you must pass the enum itself here.\n  @builder.field(UserRoleEnum)\n  @builder.nonNull()\n  @builder.description('The role of this user.')\n  role: UserRoleEnum;\n\n  @builder.nonNull()\n  @builder.field(GraphQLString)\n  firstName: string;\n\n  @builder.nonNull()\n  @builder.field(GraphQLString)\n  lastName: string;\n\n  @builder.nonNull()\n  // This special modifier makes sure our list items can't be nulls.\n  @builder.nonNullItems() // for use with lists only!\n  // Use this instead of `@field()` when defining lists!\n  // This defines a list of Users that accepts a filter argument.\n  @builder.list(() =\u003e User, { args: { filter: { type: GraphQLString, defaultValue: null } } })\n  // Define a custom resolver to filter the friends list! \n  // Can be done more efficiently if you do this in the query instead. Since in that case, \n  //   you'll have the chance to filter using a database query instead of searching the array. \n  @builder.resolver((user: User, args: { filter: string }) =\u003e {\n    if (args.filter !== null)\n      // Only return friends with a first name containing the filter string. \n      return user.friends.filter(friend =\u003e friend.firstName.toLowerCase().indexOf(filter.toLowerCase()) \u003e= 0);\n    else // no filter supplied\n      return user.friends;\n  })\n  friends: User[];\n\n  // You can define queries as static members of your classes,\n  // they will be moved to the schema's `Query` type automatically.\n  @builder.query({\n    // This defines the return type of your query.\n    // Notice how we return a custom type (User) and not a primitive one here.\n    // You can do this anywhere where a type is expected by passing\n    //    a thunk that returns your custom type!\n    returnType: { type: () =\u003e User },\n    description: 'Get the currently logged in user.'\n  })\n  static async currentUser(rootValue: any, args: any, context: TheoreticalContextInterface): Promise\u003cUser | null\u003e {\n    // You'll have to set up the context/services yourself, \n    // this is just a basic example that assumes an imaginary API.\n    return (await context.getCurrentUser()) || null;\n  }\n\n  // Mutations too!\n  @builder.mutation({\n    returnType: { type: GraphQLBoolean },\n    description: 'Ends the current session.',\n    args: { \n      confirmation: { \n        type: GraphQLBoolean,\n        description: 'This argument must be supplied, and must be true to really log out' \n      } \n    }\n  })\n  static async logout(rootValue: any, args: { confirmation: boolean }, context: TheoreticalContextInterface): Promise\u003cboolean\u003e {\n    if (args.confirmation)\n      return await context.logOut();\n    else\n      return false;\n  }\n}\n```\n\nNext, let's try our hand at defining a subscription!\n\n```ts\n\nimport { PubSub, withFilter } from 'graphql-subscriptions';\n\nconst pubSub = new PubSub();\n\n// This type will hold all the information we have about a chat message.\n@builder.type({ description: 'A chat message.', interfaces: () =\u003e [Node] })\nexport class ChatMessage extends Node {\n\n  // The text of the message\n  @builder.field(GraphQLString)\n  @builder.nonNull()\n  text: string;\n\n  // The source user, or `null` for anonymous messages.\n  @builder.field(() =\u003e User)\n  user: User | null;\n\n  // The name of the chat room this message was posted in.\n  @builder.nonNull()\n  @builder.field(GraphQLString)\n  room: string;\n\n  // Define the subscription. Note how there's an extra `subscribe` function\n  // we have to supply here.\n  @builder.subscription({\n    returnType: { type: () =\u003e ChatMessage },\n    args: { room: { type: GraphQLString, defaultValue: 'lobby' } },\n    description: 'Fired when a user sends a message.',\n    // This callback is responsible for hooking the subscription.\n    // See docs for `graphql-subscriptions` for more information.\n    subscribe: withFilter(() =\u003e pubSub.asyncIterator('message'), (payload, variables) =\u003e {\n      return payload.message.room === variables.room;\n    })\n  })\n  static async chatMessage({ message }: { message: ChatMessage }, args: any) {\n    // This resolver can be used to transform the messages\n    // before sending them to the client.\n    // We'll just make double sure that the room matches,\n    // which is already performed by `withFilter`\n    // above but there's not much else to do.\n    return message.room === args.room ? message : null;\n  }\n}\n```\n\nWe're almost done! We have everything properly defined.\n \nNow let's do the last step and build the schema!\n\n```ts\nconst schema = builder.build(); returns an instance of GraphQLSchema\n\nconsole.log(printSchema(schema));\n```\n\nAt this junction our schema will be printed to the console, and it will look like this:\n\n```graphqls\nschema {\n  query: Query\n  mutation: Mutation\n  subscription: Subscriptions\n}\n\n\"\"\"A chat message. \"\"\"\ntype ChatMessage implements Node {\n  \"\"\"Base type of this Node.\"\"\"\n  kind: String!\n  \"\"\"A unique ID for this object.\"\"\"\n  id: ID!\n  text: String!\n  user: User\n  room: String!\n}\n\n\"\"\"The root mutation object.\"\"\"\ntype Mutation {\n  \"\"\"Ends the current session.\"\"\"\n  logout: Boolean\n}\n\ninterface Node {\n  \"\"\"Base type of this Node.\"\"\"\n  kind: String!\n  \"\"\"A unique ID for this object.\"\"\"\n  id: ID!\n}\n\n\"\"\"The root query object.\"\"\"\ntype Query {\n  \"\"\"Get the currently logged in user.\"\"\"\n  currentUser: User\n}\n\n\"\"\"The root subscriptions object.\"\"\"\ntype Subscriptions {\n  \"\"\"Fired when a user sends a message.\"\"\"\n  chatMessage(room: String = \"lobby\"): ChatMessage\n}\n\n\"\"\"A user.\"\"\"\ntype User implements Node {\n  \"\"\"Base type of this Node.\"\"\"\n  kind: String!\n  \"\"\"A unique ID for this object.\"\"\"\n  id: ID!\n  firstName: String!\n  lastName: String!\n\n  \"\"\"The role of this user.\"\"\"\n  role: UserRole!\n}\n\n\"\"\"A user's role.\"\"\"\nenum UserRole {\n  \"\"\"An administrator.\"\"\"\n  administrator\n\n  \"\"\"A moderator.\"\"\"\n  moderator\n\n  \"\"\"A subscriber.\"\"\"\n  subscriber\n}\n```\n\nAt this point you can use the schema as normal.\n\n```ts\ngraphql(schema, `\n  query testQuery {\n    currentUser {\n      id\n      firstName\n      lastName\n      role\n    }\n  }\n`).then(console.log, console.error);\n```\n\n##### What about GraphQL Unions?\n\nGraphQL unions are not currently supported in version `1.0.0`. \n\nI might decide to add this feature later if there's enough demand for it, or if somebody else submits a pull request! That's always welcome!\n\n### Contributions\n\nAll contributions and pull requests are welcome. \n\nPlease make sure that test coverage does not drop below the set limits in `package.json`.\n\n### License (MIT)\n\nCopyright (c) 2018 Abdullah A. Hassan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvoodooattack%2Fmetanoia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvoodooattack%2Fmetanoia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvoodooattack%2Fmetanoia/lists"}