{"id":13510318,"url":"https://github.com/spikenail/spikenail","last_synced_at":"2025-04-07T15:10:24.614Z","repository":{"id":13268572,"uuid":"73947735","full_name":"spikenail/spikenail","owner":"spikenail","description":"A GraphQL Framework for Node.js","archived":false,"fork":false,"pushed_at":"2022-04-07T13:38:51.000Z","size":206,"stargazers_count":349,"open_issues_count":27,"forks_count":3,"subscribers_count":18,"default_branch":"master","last_synced_at":"2024-10-30T08:34:44.881Z","etag":null,"topics":["framework","graphql","graphql-api","graphql-server","graphql-server-framework","node","node-framework","nodejs","nodejs-framework","real-time","realtime"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spikenail.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-11-16T18:15:23.000Z","updated_at":"2024-09-24T16:47:08.000Z","dependencies_parsed_at":"2022-08-07T07:01:11.462Z","dependency_job_id":null,"html_url":"https://github.com/spikenail/spikenail","commit_stats":null,"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spikenail%2Fspikenail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spikenail%2Fspikenail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spikenail%2Fspikenail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spikenail%2Fspikenail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spikenail","download_url":"https://codeload.github.com/spikenail/spikenail/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247675607,"owners_count":20977378,"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":["framework","graphql","graphql-api","graphql-server","graphql-server-framework","node","node-framework","nodejs","nodejs-framework","real-time","realtime"],"created_at":"2024-08-01T02:01:33.458Z","updated_at":"2025-04-07T15:10:24.578Z","avatar_url":"https://github.com/spikenail.png","language":"JavaScript","funding_links":["https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=XQKACYYHMX23U"],"categories":["JavaScript","Libraries","nodejs"],"sub_categories":["JavaScript Libraries"],"readme":"# \u003cimg src=\"logo/logo-title.png\" height=\"100\" /\u003e\n\nSpikenail is an open-source Node.js ES7 framework which allows you to build GraphQL API with little or no coding.\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://npmjs.org/package/spikenail\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/spikenail.svg?style=flat-square\"\n         alt=\"NPM Version\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://npmjs.org/package/spikenail\"\u003e\n    \u003cimg src=\"http://img.shields.io/npm/dm/spikenail.svg?style=flat-square\"\n         alt=\"Downloads\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://david-dm.org/spikenail/spikenail.svg\"\u003e\n    \u003cimg src=\"https://david-dm.org/spikenail/spikenail.svg?style=flat-square\"\n         alt=\"Dependency Status\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://github.com/spikenail/spikenail/blob/master/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/l/spikenail.svg?style=flat-square\"\n         alt=\"License\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=XQKACYYHMX23U\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat-square\"\n           alt=\"Donate\"\u003e\n  \u003c/a\u003e\n\n\u003c/p\u003e\n\n## Features\n\nFull support of ES7 features\n\nNative GraphQL support\n\nReal-Time: GraphQL Subscriptions\n\nRelay compatible API\n\nEasy to define access control of any complexity:\nnested relations, scopes, custom dynamic roles\n\nAdvanced schema definition: virtual fields, custom resolvers\n\nValidations\n\nFlexibility: easy to adjust or override every part of a framework\n\n## Examples\n\nCreating Trello-like API:\nhttps://medium.com/@igor3489_46897/creating-advanced-graphql-api-quickly-using-spikenail-80ce6fd675ab\n\n## Install\n\n```\nnpm install -g generator-spikenail\nyo spikenail\n```\n\n## Core concepts\n\nAn ability to build the API just by configuring is the main idea of Spikenail.\nThis configuration might include relations, access control, validations and everything else we need.\n\nAt the same time, we should provide enough flexibility by allowing to adjust or override every action Spikenail does.\nFrom this point of view, Spikenail provides an architecture and a default implementation of it.\n\nThe configuration mentioned above stored in models.\n\nExample of the model `models/Item.js`:\n\n```js\nimport { MongoDBModel } from 'spikenail';\n\nclass Item extends MongoDBModel {\n\n  /**\n   * Example of a custom method\n   */\n  customMethod() {\n    // Access an underlying mongoose model\n    return this.model.find({ 'category': 'test' }).limit(10);\n  }\n}\n\nexport default new Item({\n  name: 'item',\n  properties: {\n    id: {\n      type: 'id'\n    },\n    name: {\n      type: String\n    },\n    description: {\n      type: String\n    },\n    position: {\n      type: Number\n    },\n    token: {\n      type: String\n    },\n    virtualField: {\n      virtual: true,\n      // Ensure dependent fields to be queried from the database\n      dependsOn: ['position'],\n      type: String\n    },\n    userId: {\n      type: 'id'\n    },\n    // Relations\n    subItems: {\n      relation: 'hasMany',\n      ref: 'subItem',\n      foreignKey: 'itemId'\n    },\n    user: {\n      relation: 'belongsTo',\n      ref: 'user',\n      foreignKey: 'userId'\n    }\n  },\n  // Custom resolvers\n  resolvers: {\n    description: async function(_, args) {\n      // It is possible to do some async actions here\n      let asyncActionResult = await someAsyncAction();\n      return asyncActionResult ? _.description : null;\n    },\n    virtualField: (_, args) =\u003e {\n      return 'justCustomModification' + _.position\n    }\n  },\n  validations: [{\n    field: 'name',\n    assert: 'required'\n  }, {\n    field: 'name',\n    assert: 'maxLength',\n    max: 100\n  }, {\n    field: 'description',\n    assert: 'required'\n  }],\n  acls: [{\n    allow: false,\n    properties: ['token'],\n    actions: '*'\n  }, {\n    allow: true,\n    properties: ['token'],\n    actions: ['create']\n  }]\n});\n```\n\n### CRUD\n\nIn Spikenail every CRUD action is a set of middlewares.\nThese middlewares are not the request middlewares and they exists separately.\n\nSome of default middlewares are:\n\n* Access control middleware\n* Validation middleware\n* Before action\n* Process action\n* After action\n\nThe whole chain can be changed in any way.\n\nFor example, you can override \"Before action\" middleware in a following way:\n\n`models/Item.js`\n\n```js\n\n  async beforeCreate(result, next, opts, input, ctx) {\n    let checkResult = await someAsyncCall();\n\n    if (checkResult) {\n        return next();\n    }\n\n    result.errors = [{\n        message: 'Custom error',\n        code: '40321'\n    }];\n  }\n\n```\n\n## Configuration\n\nConfiguration files are stored under `config` folder\n\n### Data sources\n\nCurrently, only MongoDB is supported.\n\nIt is recommended to store all configurations using environment variables\n\nExample of `config/sources.js`\n\n```js\nexport default {\n  'default': {\n    adapter: 'mongo',\n    connectionString: process.env.SPIKENAIL_MONGO_CONNECTION_STRING\n  }\n}\n```\n\n## GraphQL API\n\n### Queries\n\n#### node\n\n```js\nnode(id: ID!): Node\n```\n\nhttps://facebook.github.io/relay/docs/graphql-object-identification.html#content\n\nExample:\n\n```js\n{\n    node(id: \"some-id\") {\n        id,\n        ... on Article {\n            title,\n            text\n        }\n    }\n}\n```\n\n#### viewer\n\nRoot field\n\n```js\nviewer: viewer\n\ntype viewer implements Node {\n  id: ID!\n  user: User,\n  allXs(): viewer_XConnection\n}\n```\n\n\n#### Query all items of a specific model (allXs)\n\nFor `Article` model:\n\n```js\nquery {\n    viewer {\n        allArticles() {\n            edges {\n            node {\n                id,\n                title,\n                text\n                }\n            }\n        }\n    }\n}\n```\n\n\n#### Query single item (getX)\n\nQuery a specific item by unique field:\n\n```js\nquery {\n    getArticle(id: \"article-id-1\") {\n        id, title, text\n    }\n}\n```\n\n#### Pagination\n\nExample:\n\n```js\n{\n    getArticle(id: \"some-id\") {\n        id\n        userId\n        user {\n            id\n            name\n        }\n        tags(first: 10, after: \"opaqueCursor\") {\n            edges {\n                node {\n                    id\n                    name\n                    itemsCount\n                }\n            }\n            pageInfo {\n                hasNextPage\n                hasPreviousPage\n                endCursor\n                startCursor\n            }\n        }\n    }\n}\n\n```\n\nSee relay documentation for more details: https://facebook.github.io/relay/graphql/connections.htm\n\n\n#### Filtering and sorting\n\nExample:\n\n```js\nquery {\n  viewer {\n    allBoards(filter: { where: { name: { regexp: \"^Public\" } }, order: \"id DESC\" }) {\n      edges {\n        node {\n          id\n          userId\n          name\n        }\n      }\n    }\n  }\n}\n```\n\n#### Mutations\n\n##### createX\n\n```js\nmutation createX(input: CreatexInput): CreatexPayload\n```\n\nExample:\n\n```js\nmutation {\n  createItem(input: { name: \"New item\", clientMutationId: \"123\" }) {\n    item {\n      id\n      name\n    }\n    clientMutationId\n    errors {\n      message\n      code\n    }\n  }\n}\n```\n\n##### updateX\n\n```js\nmutation updateX(input: UpdatexInput): UpdatexPayload\n```\n\nExample:\n\n```js\nmutation {\n  updateItem(input: { name: \"New item name\", clientMutationId: \"123\" }) {\n    item {\n      id\n      name\n    }\n    clientMutationId\n    errors {\n      message\n      code\n    }\n  }\n}\n```\n\n##### removeX\n\n```js\nmutation removeX(input: RemovexInput): RemovexPayload\n```\n\nExample:\n\n```js\nmutation {\n  removeItem(input: { id: \"Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjhh\" }) {\n    removedId\n    errors {\n      code\n      message\n    }\n  }\n}\n```\n\n#### Subscriptions\n\nFirst of all, you need to install a needed PubSub adapter:\n\n```\nnpm install --save spikenail-pubsub-redis\n```\n\nThen, create a `config/pubsub.js` file to enable subscriptions:\n\n```js\nexport default {\n  pubsub: {\n    adapter: 'redis'\n  }\n}\n```\n\nWhen the server is started, you can go to the http://localhost:5000/graphiql\nto open in-browser IDE which supports GraphQL subscriptions.\n\nDefault WebSocket endpoint is ws://localhost:8000/graphql\n\n##### WebSockets authentication\n\nIt’s not possible to provide custom headers when creating WebSocket connection in browser.\nYou to pass `auth_token` as query parameter, e.g. ws://localhost:8000/graphql?auth_token=igor-secret-token\n\n##### subscribeToX\n\nExamples:\n\nSubscribe to all Items:\n\n```js\nsubscription {\n  subscribeToItem {\n    mutation\n    node {\n      id\n      name\n      user {\n        id\n        name\n      }\n      nesteditems {\n        edges {\n          node {\n            id\n            name\n          }\n        }\n      }\n    }\n    previousValues {\n      id\n    }\n  }\n}\n```\n\nSubscribe to only particular item changes:\n\n```js\nsubscription {\n  subscribeToItem(filter: { where: { id: \"Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjg4\" } }) {\n    mutation\n    node {\n      id\n      name\n      user {\n        id\n        name\n      }\n      nesteditems {\n        edges {\n          node {\n            id\n            name\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nSubscribe to all Books in specified Category:\n\n```js\nsubscription {\n  subscribeToBook(filter: { where: { categoryId: \"Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjg4\" } }) {\n    mutation\n    node {\n      id\n      title\n      author {\n        id\n        name\n      }\n    }\n  }\n}\n```\n\n\n\n## Defining a Model\n\n### Using model generator\n\nYou can use model generator in order to simplify model creation:\n\n```\nyo spikenail:model board\n```\n\nThis will create models/Board.js file with only id field:\n\n```js\nimport { MongoDBModel } from 'spikenail';\n\nclass Board extends MongoDBModel {}\n\nexport default new Board({\n  name: 'Board',\n  properties: {\n    id: {\n      type: 'id'\n    }\n  }\n});\n```\n\n### Relations\n\n#### hasMany relation\n\n`models/Book.js`\n\n```js\nproperties: {\n    authors: {\n      relation: 'hasMany',\n      ref: 'author',\n      foreignKey: 'bookId'\n    }\n}\n```\n\n`authors` definition could be simplified:\n\n```js\nauthors: {\n  relation: 'hasMany'\n}\n\n```\n\nIn this case framework will try to guess other parameters.\n\n##### Custom hasMany condition\n\n```js\n getCondition: function(_) {\n    let names = _.map(i =\u003e i.name);\n    return { otherModelField: { '$in': names } }\n }\n```\n\n#### belongsTo relation\n\n```js\nlist: {\n    relation: 'belongsTo'\n    ref: 'list',\n    foreignKey: 'listId'\n}\n```\n\nSimplified definition:\n\n```js\nlist: {\n    relation: 'belongsTo'\n}\n```\n\n\n#### MongoDBModel\n\nUnderlying model is a [mongoose](http://mongoosejs.com/) model. You can access it through `this.model`\n\n##### Changing collection name\n\n```js\nproviderOptions: {\n    collection: 'customName'\n}\n```\n\n## Authentication\n\n### Simple token authentication middleware\n\nSpikenail has built-in middleware for the authentication.\n\nIt looks for `tokens` array stored in `User` model in a following format:\n\n```js\n[{\n    token: \"user-random-token\"\n}, {\n    token: \"user-random-token-2\"\n}]\n```\n\nThe current user will be placed in context and accessible through `ctx.currentUser`\n\n## ACL\n\n### Introduction\n\nACL rules are specified under the `acls` property of the model schema. Rules are processed by framework one by one in a natural order.\nThere is no any access restrictions by default.\n\nTake a look at a below example:\n\n```js\nacls: [{\n    allow: false,\n    roles: ['*'],\n    actions: ['*']\n}, {\n    allow: true,\n    roles: ['*'],\n    actions: ['*'],\n    scope: function() {\n        return { isPublic: true }\n    }\n}\n```\n\nThe first rule here is disable everything for everyone:\n\n```js\n{\n    allow: false,\n    roles: ['*'],\n    actions: ['*']\n}\n```\n\nThe second rule allows everything if `isPublic` property of a item equals `true`.\n\nRules notation could be simplified and above rules might be written as:\n\n```js\nacls: [{\n    allow: false\n}, {\n    allow: true\n    scope: function() {\n        return { isPublic: true }\n    }\n}\n\n```\n\n### Rule structure\n\n#### allow\n\nEach rule must have the `allow` property defined. `allow` is a boolean value\nthat indicates if a rule allows something or disallows.\n\nExample:\n\n```js\nallow: true\n```\n\n#### properties (optional)\n`properties` is an array of properties of a model that rule should apply to.\nOmit or use * sign to apply to all rules.\n\n#### actions (optional)\n\nSpecify what actions rule should be applied to.\nThere are 4 types of actions:\n\n* create\n* update\n* remove\n* read\n\nOmit this property or use * sign to apply to all actions.\n\nExample:\n\n```js\nactions: ['create', 'update']\n```\n\n#### scope\n\nScope is a MongoDB condition. Rule will be applied only to those documents that match the scope.\n\nExample\n\n```js\n{ isPublic: true }\n```\n\nThe rule will be applied only to documents that have `isPublic` property equals `true`.\n\nScope can be defined as a function. In this case you have an access to the context variable:\n\n```js\nscope: function(ctx) {\n    return { isPublic: true }\n}\n```\n\n#### roles\n\n`roles` is an array of roles that rule should apply to.\n\nExample\n\n```js\nroles: ['anonymous', 'member']\n```\n\nRoles might be static or dynamic.\n\n#### Static roles\n\nStatic roles are roles that not depend on a particular document or a data set.\nThey are calculated once per a request for a current user.\n\nBuilt-in static roles are:\n\n* anonymous\n* user\n\n##### Adding your own static roles\n\nOverride the `getStaticRoles` function of the model.\n\n#### Dynamic roles\n\nDynamic roles are calculated for each particular document.\nFor example, role `owner` means that `currentUser.id === fetchedDocument.id`\n\nBuilt-in dynamic roles are:\n\n* owner\n\n###### Defining dynamic roles\n\nDynamic roles are defined using `roles` object of the model schema.\n\nFor example, we have `members` array where sharing information stored in a following format:\n\n\n```js\n[{\n    userId: 123\n    role: 'member'\n}, {\n    userId: 456,\n    role: 'observer'\n}]\n```\n\nThen we can define a role `member` in the model schema:\n\n```js\nroles: {\n member: {\n   cond: function(ctx) {\n     return { 'members': { '$elemMatch': { 'userId': ctx.currentUser.id, role: 'member' } } }\n   }\n }\n}\n```\n\nAnd use it in the roles property of ACL rule:\n\n```js\nroles: ['member']\n```\n\n\n#### Access based on another model\n\nIn some cases we want to apply rule only if another model satisfies some condition.\nWe can use the checkRelation property for that.\n\n##### checkRelation\n\nExample:\n\n`Article.js` model has defined belongsTo relation\n\n```js\nblog: {\n    relation: 'belongsTo'\n}\n```\n\nWe want allow for `user` to read an article only if he can read the blog it belongs to:\n\n```js\nacls: [{\n    allow: false\n}, {\n    allow: true,\n    roles: ['user'],\n    actions: ['read'],\n    checkRelation: {\n        name: 'blog',\n        action: 'read'\n    }\n}]\n```\n\nIf checkRelation condition is not satisfied, the rule will not be applied at all.\nIt means that `allow: true` will not become `allow: false` and vice versa. Rule will be filtered out.\n\n## Validations\n\nUsually the data that we receive from users needs to be validated. It is easy to do with Spikenail.\nFor example, we want `name` to be required property and its length to not exceed 50 characters. \nThis could be done in following way:\n\n`models/Item.js`\n\n```js\nvalidations: [{\n  field: 'name',\n  assert: 'required'\n}, {\n  field: 'name',\n  assert: 'maxLength',\n  max: 50\n}]\n```\n\n## Future plans\n\nSQL databases support\n\nSimple endpoint (non-relay)\n\n## Support\n\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=XQKACYYHMX23U)\n\n## License\n\nMIT © [Igor Lesnenko](http://github.com/spikenail)\n\n[npm-url]: https://npmjs.org/package/spikenail\n[npm-image]: https://img.shields.io/npm/v/spikenail.svg?style=flat-square\n\n[depstat-url]: https://david-dm.org/spikenail/spikenail\n[depstat-image]: https://david-dm.org/spikenail/spikenail.svg?style=flat-square\n\n[download-badge]: http://img.shields.io/npm/dm/spikenail.svg?style=flat-square\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspikenail%2Fspikenail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspikenail%2Fspikenail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspikenail%2Fspikenail/lists"}