{"id":18274809,"url":"https://github.com/lucid-services/serviser-restfulness","last_synced_at":"2026-05-17T15:33:43.477Z","repository":{"id":34294037,"uuid":"168033324","full_name":"lucid-services/serviser-restfulness","owner":"lucid-services","description":"instant REST API","archived":false,"fork":false,"pushed_at":"2022-12-10T16:47:01.000Z","size":1307,"stargazers_count":0,"open_issues_count":13,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-09T19:33:10.347Z","etag":null,"topics":["ajv","automation","instant-api","json-schema","rest-api","serviser"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lucid-services.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2019-01-28T20:38:46.000Z","updated_at":"2022-02-11T12:12:10.000Z","dependencies_parsed_at":"2023-01-15T05:58:57.378Z","dependency_job_id":null,"html_url":"https://github.com/lucid-services/serviser-restfulness","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/lucid-services/serviser-restfulness","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucid-services%2Fserviser-restfulness","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucid-services%2Fserviser-restfulness/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucid-services%2Fserviser-restfulness/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucid-services%2Fserviser-restfulness/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lucid-services","download_url":"https://codeload.github.com/lucid-services/serviser-restfulness/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucid-services%2Fserviser-restfulness/sbom","scorecard":{"id":602537,"data":{"date":"2025-08-11","repo":{"name":"github.com/lucid-services/serviser-restfulness","commit":"d737f0ccfcc715e6c9d310ceb2d3f64521980a40"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.6,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/26 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: GNU General Public License v3.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: containerImage not pinned by hash: Dockerfile:1: pin your Docker image by updating lucidservices/node:lts-alpine to lucidservices/node:lts-alpine@sha256:c00840604a10a9ecf51af661336d06177d89f40e87d833261cd0d265b722a5a8","Info:   0 out of   1 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 4 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"55 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-fwr7-v2mv-hh25","Warn: Project is vulnerable to: GHSA-42xw-2xvc-qx8m","Warn: Project is vulnerable to: GHSA-4w2v-q235-vp99","Warn: Project is vulnerable to: GHSA-cph5-m8f7-6c5x","Warn: Project is vulnerable to: GHSA-wf5p-g6vw-rhxx","Warn: Project is vulnerable to: GHSA-jr5f-v2jv-69x6","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-h452-7996-h45h","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-rv95-896h-c2vc","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-qrmc-fj45-qfc2","Warn: Project is vulnerable to: GHSA-2j2x-2gpw-g8fm","Warn: Project is vulnerable to: GHSA-74fj-2j2h-c42q","Warn: Project is vulnerable to: GHSA-pw2r-vq6v-hr8c","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-4jv9-3563-23j3","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-4xcv-9jjx-gfj3","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-wrvr-8mpx-r7pp","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-6xwr-q98w-rvg7","Warn: Project is vulnerable to: GHSA-q674-xm3x-2926","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-gqgv-6jq5-jjj9","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-4rq4-32rv-6wp6","Warn: Project is vulnerable to: GHSA-64g7-mvw6-v9qj","Warn: Project is vulnerable to: GHSA-8225-6cvr-8pqp","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T00:47:06.569Z","repository_id":34294037,"created_at":"2025-08-21T00:47:06.569Z","updated_at":"2025-08-21T00:47:06.569Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279007028,"owners_count":26084227,"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","status":"online","status_checked_at":"2025-10-11T02:00:06.511Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["ajv","automation","instant-api","json-schema","rest-api","serviser"],"created_at":"2024-11-05T12:10:49.962Z","updated_at":"2025-10-11T21:14:59.910Z","avatar_url":"https://github.com/lucid-services.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/lucid-services/serviser-restfulness.svg?branch=master)](https://travis-ci.org/lucid-services/serviser-restfulness)  [![Test Coverage](https://codeclimate.com/github/lucid-services/serviser-restfulness/badges/coverage.svg)](https://codeclimate.com/github/lucid-services/serviser-restfulness/coverage) [![npm version](https://badge.fury.io/js/serviser-restfulness.svg)](https://www.npmjs.com/package/serviser-restfulness)  \n\n\n`serviser-restfulness` is yet another `serviser` plugin that will help you with your REST API implementation by generating fully functional API endpoints that operate on your ralational database backed resources.  \nThat being said, its up to the user to decide when it's more appropriate to implement a route from scratch due to its increasing complexity.\n\nThe following is a simplest example of what it takes to design basic REST API operations:  \n\n```javascript\n    //define resources\n    const Resource = require('serviser-restfulness').Resource;\n\n    const movie = new Resource({\n        singular: 'movie',\n        plural: 'movies',\n        properties: {\n            name: {type: 'string', pattern: '^[a-zA-Z0-9 ]+$'},\n            description: {type: 'string', maxLength: 256},\n            released_at: {type: 'string', format: 'date'}\n        },\n        responseProperties: {\n            id: {type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER},\n            name: {$ref: 'movie.name'},\n            released_at: {$ref: 'movie.released_at'},\n            rating: {type: 'number', minimum: 0, maximum: 10}\n        }\n    });\n\n    const app = service.buildApp('public');\n\n    //define http routers\n    const movies = app.buildRestfulRouter({\n        url: '/api/{version}/@movies',\n        version: 1.0\n    });\n\n    //define http endpoints\n    movies.get('/:{key}');//get movies\n    movies.get('/');//get movies\n    movies.post('/');//create new movie\n    movies.put('/:{key}');//update a movie\n    movies.del('/:{key}');//delete a movie\n    movies.del('/');//delete movies\n```\n\nThats it, you are done. You have created 6 fully functional API endpoints!  \nWith assumption that you were to use `serviser-doc` plugin, you'd get [THIS generated API documentation](https://fogine.github.io/serviser-restfulness-documentation-example) of above defined endpoints.  \nOn the other hand if you were to plug in `serviser-sdk` you would get client API SDKs for free.  \n\nOf cource, there is more to be familiarized with when defining REST operations using `serviser-restfulness`:  \n\n* [Resource definition](#resource-definition)\n    * [constructor options](#constructor-options)\n    * [json-schema column references](#json-schema-column-references)\n    * [associations](#associations)\n    * [API](#api)\n        * [Resource.registry](#resrouceregistry)\n            * [Resource.registry.getByPluralName](resourceregistrygetbypluralname)\n            * [Resource.registry.getBySingularName](resourceregistrygetbysingularname)\n        * [Resource.prototype.getName()](resourceprototypegetname)\n        * [Resource.prototype.getPluralName()](resourceprototypegetpluralname)\n        * [Resource.prototype.getTableName()](resourceprototypegettablename)\n        * [Resource.prototype.getKeyName()](resourceprototypegetkeyname)\n        * [Resource.prototype.prop()](resourceprototypeprop)\n        * [Resource.prototype.hasProp()](resourceprototypehasprop)\n        * [Resource.prototype.query(knex)](resourceprototypequery)\n        * [Resource.prototype.belongsTo()](resourceprototypebelongstoresource-options)\n        * [Resource.prototype.hasMany()](resourceprototypehasmanyresource-options)\n        * [Resource.prototype.belongsToMany()](resourceprototypebelongstomanyresource-options)\n* [Route definition](#route-definition)\n    * [list of supported REST operations](#list-of-supported-rest-operations)\n    * [customizing route](#customizing-route)\n        * [modifying response data](#response-properties)\n        * [modifying accepted input data](#input-data-validation)\n    * [about authentication/restricting access](#about-authenticationrestricting-access)\n    * [request lifecycle events and implementing extra logic](#request-lifecycle-events-and-implementing-extra-logic)\n* [Global configuration options](#global-configuration-options)\n\n## Resource definition\n\nResources are compound data structures describing a data source and how it relates to `RDS` (relational database storage).  \n\n**Disclaimer:** This library is NOT a `ORM`. Resources do NOT implement `active-record` pattern.  \n\n### constructor options\n\n- `singular` - _required_ singular resource name\n- `plural` - _required_ plural resource name\n- `db` - options related to `RDS`\n    - `table` - _required_, _default_ equals to `plural` resource name\n    - `key` - primary key options\n        - `name` - _required_, _default_:`id`\n        - `type` - _required_, _default_:`integer` enum: [`integer`, `string`]\n        - `format` - _optional_ `ajv` string format\n        - `pattern` - _optional_ `ajv` string regex\n- `timestamps` - _optional_, _default_ false, sets `created_at` \u0026 `updated_at` timestamps every time the resource is created/updated\n- `softDelete` - _optional_, _default_ false, DELETE methods set `deleted_at` timestamp instead of permanently removing the record\n- `properties` - _required_, `ajv-json-schema` object properties descriptor, lists allowed input properties for REST operations\n- `responseProperties` - _optional_, `ajv-json-schema` object properties descriptor, lists allowed query and response properties, if NOT defined, ALL `properties` will be whitelisted\n- `dynamicDefaults` - _optional_, `ajv` [dynamic defaults](https://github.com/epoberezkin/ajv-keywords#dynamicdefaults) definition object for properties listed in `properties` option object\n- `CREATED_AT` - _optional_, _default_ `created_at` allows to customize timestamp property name\n- `UPDATED_AT` - _optional_, _default_ `updated_at` allows to customize timestamp property name\n- `DELETED_AT` - _optional_, _default_ `deleted_at` allows to customize timestamp property name\n\nThere are two `Resource` contructor options `properties` and `responseProperties` holding resource property definitions.  \n- `properties` option describes `columns` whose values might be set through an API request, usually as part of json-payload of `POST` \u0026 `PUT` endpoints\n- `responseProperties` option describes `columns` that routes can respond with. This is also default whitelist of properties the user of the API can filter resultset by.  \n\nThe developer can of course overwrite input \u0026 response definitions on per route basis (see [customizing route](#customizing-route))  \n\n### json-schema column references\n\n```javascript\n    const user = new Resource({\n        singular: 'user',\n        plural: 'users',\n        properties: {\n            name: {type: 'string', maxLength: 32},\n            password: {type: 'string'}\n        },\n        responseProperties: {\n            id: {type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER},\n            //reference the property schema from inside of the owning resource\n            name: {$ref: 'user.name'}\n        }\n    });\n\n    const post = new Resource({\n        singular: 'post',\n        plural: 'posts',\n        properties: {\n            title: {type: 'string', maxLength: 128},\n            content: {type: 'string', maxLength: 2048},\n            //reference user id from another resource\n            user_id: {$ref: 'user.id'}\n        }\n    });\n```\n\n- Resource property schemas defined as part of `properties` \u0026 `responseProperties` constructor options will get registered with [Application](https://lucid-services.github.io/serviser/App.html) wide `Ajv` validator instance allowing the user to reference property schema from outside of a resource the property belongs to.  \nProperties which reference other properties in its schema will NOT be registered with the `Ajv` instance. For example you cant reference `post.user_id` property from the above code example.  \n\n- Note that in order for `json-schema` references to work, resource objects have to be instantiated before you create your [HttpApplication](https://lucid-services.github.io/serviser/App.html) otherwise you get an error in following format:  \n\n\u003e Error: can't resolve reference resource-name.column from id #  \n\n### associations\n\nWhen designing a route which operates on multiple resources, eg:  \n\u003e /api/v1.0/@users/:{key}/@posts/:{key}  \n\nthe user must tell `serviser-restfulness` how the resources are related to each other in order for the operation to function properly: \n\n```javascript\n//defines relation of user to the post and relation of post to the user.\nuser.hasMany(post);\n// or (both are redundant)\npost.belongsTo(user);\n```\n\nSee [Resource.prototype.belongsTo](#resourceprototypebelongstoresource-options)  \nor/and [Resource.prototype.hasMany](#resourceprototypehasmanyresource-options)  \nor/and [Resource.prototype.belongsToMany](#resourceprototypebelongstomanyresource-options) respectivelly.\n\n\n### API\n\n#### `Resource.registry`\nis global `ResourceRegistry` instance\n#### `Resource.registry.getByPluralName()`\nretrieves a resource by its plural unique name\n#### `Resource.registry.getBySingularName()`\nretrieves a resource by its singular unique name\n#### `Resource.prototype.getName(count)`\nreturns singular resource name by default, if `count` argument value is greater than 1 then it returns plural version of the name.\n#### `Resource.prototype.getPluralName()`\nreturns plural version of the name.\n#### `Resource.prototype.getTableName()`\nreturns `sql` table name as defined by `db.table` constructor option\n#### `Resource.prototype.getKeyName()`\nreturns name of resource primary key\n#### `Resource.prototype.prop()`\nreturns property schema as defined either in `properties` or `responseProperties` object. `properties` object takes precendence.\n#### `Resource.prototype.hasProp()`\nreturns `boolean` value. True if property is defined either in `properties` or `responseProperties` object.\n#### `Resource.prototype.query()`\n```javascript\n/*\n * @param {Knex} knex\n * return {QueryBuilder}\n */\n```\nreturns `knex` query builder for resource table. Automaticaly handles table `timestamps`\n#### `Resource.prototype.belongsTo(resource, options)`\n```javascript\n/*\n * @param {Resource} resource\n * @param {Object} [options]\n * @param {String} [options.foreignKey]\n * @param {String} [options.localKey]\n */\n```\nDefines One to One association between `sourceResource.belongsTo(targetResource)`.  \n* `localKey` option defaults to `\u003ctargetResourceSingularName\u003e_\u003ctargetResourcePrimaryKeyName\u003e`\n* `foreignKey` option defaults to `\u003ctargetResourcePrimaryKeyName\u003e`\n\n#### `Resource.prototype.hasMany(resource, options)`\n```javascript\n/*\n * @param {Resource} resource\n * @param {Object} [options]\n * @param {String} [options.foreignKey]\n * @param {String} [options.localKey]\n */\n```\nDefines One to Many association between `sourceResource.hasMany(targetResource)`.  \n* `localKey` option defaults to `\u003csourceResourcePrimaryKeyName\u003e`\n* `foreignKey` option defaults to `\u003csourceResourceSingularName\u003e_\u003csourceResourcePrimaryKeyName\u003e`\n\n#### `Resource.prototype.belongsToMany(resource, options)`\n```javascript\n/*\n * @param {Resource} resource\n * @param {Object} options\n * @param {String} [options.foreignKey]\n * @param {String} [options.localKey]\n * @param {Object} [options.through]\n * @param {Resource} [options.through.resource]\n * @param {String} [options.through.localKey]\n * @param {String} [options.through.foreignKey]\n */\n```\nDefines Many to Many association between `sourceResource.belongsToMany(targetResource)`.  \n* `localKey` option defaults to `\u003csourceResourcePrimaryKeyName\u003e`\n* `foreignKey` option defaults to `\u003ctargetResourcePrimaryKeyName\u003e`\n* `through.resource` option defaults to generated pivot resource object whose singular and plural name is set to alphabetically sorted and concatenated singular and plural names of source and target resources respectivelly.  \n    eg: for source and target resources `users.belongsToMany(movies);` default pivot singular name would be `movie_user` and plural `movies_users`\n* `through.localKey` option defaults to `\u003csourceResourceSingularName\u003e_\u003csourceResourcePrimaryKeyName\u003e`\n* `through.foreignKey` option defaults to `\u003ctargetResourceSingularName\u003e_\u003ctargetResourcePrimaryKeyName\u003e`\n\n## Route definition\n\n### list of supported REST operations\n\n#### single resource queries\n\n- `GET @resource/:{key}` - fetch resource response properties by primary key\n- `GET @resource/:property` - fetch resource response properties by any underlying table column\n- `GET @resource` - retrieve paginated and optionally sorted collection of all or filtered resources\n- `POST @resource/:{key}` - create a new resource whose primary key type is NOT auto-incremented integer\n- `POST @resource` - create a new resource whose primary key IS auto-incremented integer\n- `PUT @resource/:{key}` - update a resource by its primary key\n- `PUT @resource/:property` - update a resource by any of its properties\n- `DELETE @resource/:{key}` - remove a resource by its primary key\n- `DELETE @resource/:property` - remove a resource by any of its properties\n- `DELETE @resource` - delete all resources or resources that match particular query filter\n\n### multi resource queries\n\n##### depending or relationship\n\n###### many to many\n\n- `GET @resource1/:{key}/@resource2/:{key}` - fetch resource2 which belongs to resource1 \n- `GET @resource1/:property/@resource2/:property` - fetch resource2 which belongs to resource1\n- `GET @resource1/:{key}/@resource2` - retrieve paginated and optionally sorted collection of all or filtered resource2 records that belong to resource1\n- `GET @resource1/:property/@resource2` - retrieve paginated and optionally sorted collection of all or filtered resource2 records that belong to resource1\n- `POST @resource1/:{key}/@resource2/:{key}` - create a new resource2 whose primary key type is NOT auto-incremented integer and associate it with resource1\n- `POST @resource1/:property/@resource2/:{key}` - create a new resource2 whose primary key type is NOT auto-incremented integer and associate it with resource1\n- `POST @resource1/:{key}/@resource2` - create a new resource2 whose primary key type IS auto-incremented integer and associate it with resource1\n- `POST @resource1/:property/@resource2` - create a new resource2 whose primary key type IS auto-incremented integer and associate it with resource1\n- `PUT @resource1/:{key}/@resource2/:{key}` - associate resource1 with resource2 by inserting a new record to pivot table\n- `PUT @resource1/:property/@resource2/:property` - associate resource1 with resource2 by inserting a new record to pivot table\n- `DELETE @resource1/:{key}/@resource2/:{key}` - deassociate resource1 and resource2 from each other\n- `DELETE @resource1/:property/@resource2/:property` - deassociate resource1 and resource2 from each other\n- `DELETE @resource1/:{key}/@resource2` - deassociate all resource1 records and resource2 records from each other where optional condition applies\n\n###### one to many\n- `GET @resource1/:{key}/@resource2/:{key}` - fetch resource2 which belongs to resource1 \n- `GET @resource1/:property/@resource2/:property` - fetch resource2 which belongs to resource1\n- `GET @resource1/:{key}/@resource2` - retrieve paginated and optionally sorted collection of all or filtered resource2 records that belong to resource1\n- `GET @resource1/:property/@resource2` - retrieve paginated and optionally sorted collection of all or filtered resource2 records that belong to resource1\n- `POST @resource1/:{key}/@resource2/:{key}` - create a new resource2 whose primary key type is NOT auto-incremented integer and associate it with resource1\n- `POST @resource1/:property/@resource2/:{key}` - create a new resource2 whose primary key type is NOT auto-incremented integer and associate it with resource1\n- `POST @resource1/:{key}/@resource2` - create a new resource2 whose primary key type IS auto-incremented integer and associate it with resource1\n- `POST @resource1/:property/@resource2` - create a new resource2 whose primary key type IS auto-incremented integer and associate it with resource1\n- `PUT @resource1/:{key}/@resource2/:{key}` - update a resource2 by its primary key whose foreign key value of resource1 is equal to particular value\n- `DELETE @resource1/:{key}/@resource2/:{key}` - remove  resource2 that is associated to resource1\n- `DELETE @resource1/:property/@resource2/:property` - remove resource2 that is associated to resource1\n- `DELETE @resource1/:{key}/@resource2` - remove all resource2 records that are associated to resource1  where optional condition applies\n\n###### one to one\n- `GET @resource1/:{key}/@resource2/:{key}` - fetch resource2 which belongs to resource1 \n- `GET @resource1/:property/@resource2/:property` - fetch resource2 which belongs to resource1\n\n\n#### spetial `_embed` query parameter\n\nAccepted by all GET routes. Eager loads associated One to One resources (embedding collections is not supported).  \nExamples:  \n- `GET posts/1?_embed=user` - embeds whole user resource in the response - meaning embeds all properties defined as part of `responseProperties` user resource option\n- `GET posts/1?_embed=user.username,user.id` - embeds user username and id in the response\n\n#### spetial `_sort` \u0026 `_limit` \u0026 `_offset` query parameters\n\nAccepted by GET routes that return a collection of resources.  \nAllows to sort records and limit number of fetched records (paginate results)\nExample:  \n- `GET users/?_sort=username,-id` - order by username ASC, id DESC\n\n#### filtering results or reducing scope of delete queries\n\nAccepted by GET and DELETE routes that operate on a collection of resources.  \nExample resource:  \n```javascript\n    const user = new Resource({\n        singular: 'user',\n        plural: 'users',\n        properties: { //results can  NOT be reduced by these properties\n            password: {type: 'string'},\n            email: {type: 'string'}\n        },\n        responseProperties: {//but can be filtered/reduced by these\n            id: {type: 'integer'},\n            username: {type: 'string'}\n        }\n    });\n```\n\nConsidering the above code example, endpoints accept the following filter parameters (by default, can be modified):  \n- `GET users/?id=1`\n- `DELETE users/?username=anonym`\n\nWhen filtering by query property of type `string`, `WHERE like %filter-value%` clause will be generated.  \n\n#### spetial `_filter` query parameter\n\nIn addition to simple query filters described above, related routes accept compound `_filter` parameter which must be a serialized json object in the following format:  \n\n```javascript\n{\n    column: schema,\n    //condition negation\n    column2: {not: schema}\n}\n\n//where schema is valid according to:\n{\n    type: 'object',\n    properties: {\n        eq: {type: ['string', 'number', 'null', 'boolean']}, //equal to\n        gt: {type: 'number'}, //greater than\n        gte: {type: 'number'}, //greater than or equal\n        lt: {type: 'number'}, //lower than\n        lte: {type: 'number'}, //lower than or equal\n        like: {type: 'string'},\n        iLike: {type: 'string'}, //case insensitive like\n        between: {\n            type: 'array',\n            maxItems: 2,\n            minItems: 2,\n            items: {type: ['string', 'number']}\n        },\n        in: {\n            type: 'array',\n            maxItems: config.getOrFail('filter:maxItems'),\n            minItems: 1,\n            items: {type: ['string', 'number']}\n        }\n    }\n}\n```\n\n`_filter` accepts only those column defined as part of route response schema.  \n\n**Explicitly define what columns can be used for reducing dataset scope through query parameters:**  \n\n\u003e route.reducesDatasetBy(['id', 'user.username']);\n\n`reducesDatasetBy()` overwrites the default whitelisted column set.  \nEmpty array disables query filters altogether\n\n### customizing route\n\n```javascript\n    const user = new Resource({\n        singular: 'user',\n        plural: 'users',\n        properties: {\n            name: {type: 'string'},\n            username: {type: 'string'},\n            password: {type: 'string'},\n            email: {type: 'string'}\n        },\n        responseProperties: {\n            id: {type: 'integer'},\n            name: {$ref: 'user.name'},\n            username: {$ref: 'user.username'},\n        }\n    });\n\n    const users = app.buildRestfulRouter({\n        url: '/api/{version}/@users',\n        version: 1.0\n    });\n```\n\n`Router`'s `get` \u0026 `post` \u0026 `put` \u0026  `del` methods all return an uninitialized `serviser` [HttpRoute](https://lucid-services.github.io/serviser/Route.html) object.  \nThe user is given time to manualy initialize the route in the current event loop tick, that is\nto define validation rules for `headers` \u0026 `body` \u0026 `query` \u0026 `params` objects and/or response schema or even to implement or tweak\nthe main route's logic.  \n`serviser-restfulness` schedules initialization procedures that will execute on the next event loop tick and will set default behavior and rules\nwhere it's not been done by the user.\n\n```javascript\nusers.get('/'); //returns Route object instance\nusers.post('/'); //returns Route object instance\nusers.put('/'); //returns Route object instance\nusers.del('/'); //returns Route object instance\n```\n\n##### response properties\n\n```javascript\nusers.get('/:{key}'); //get single user\n```\n\nthe get user route, if not modified by the developer, returns `id` \u0026 `name` \u0026 `username` properties for every user.  \nLets modify the route so that it returns just the user username:  \n\n```javascript\nusers\n.get('/:{key}')\n.respondsWith({\n    type: 'object',\n    additionalProperties: false,//always make sure to filter out any unexpected properties\n    properties: {\n        username: {$ref: 'user.username'}\n    }\n});\n```\nsimple, right? You just define a custom `json-schema`.   \nThat being said, by defining your cuctom response schema, you are essentially overwriting defaults.  \nFor example the above custom schema does not allow any associated resources to be embedded along.\n\n##### input data validation\n\n```javascript\nusers.post('/'); //register new user\n```\n\nAgain, the post users route, if not modified by the developer, accepts in its json payload all properties that are defined as part of user's resource `properties` constructor option.  \n\nIn this case, the route accepts `name` \u0026 `username` \u0026 `password` \u0026 `email` properties.  \nLets modify the route so that it accepts additional `password_confirmation` field:  \n\n```javascript\nusers\n.post('/')\n.validate({\n    type: 'object',\n    additionalProperties: false,//always make sure to filter out any unexpected properties\n    properties: {\n        name: {$ref: 'user.name'},\n        username: {$ref: 'user.username'},\n        password: {\n            $ref: 'user.password',\n            bcrypt: {saltLength: 8}\n        },\n        password_confirmation: {\n            const: {$data: '1/password'}\n        },\n        email: {$ref: 'user.email'}\n    }\n}, 'body');\n```\nIn the custom payload validator schema above, we make sure `password_confirmation` field matches the `password` field.  \n\nThere is one more thing we did and thats we applied our custom validation/sanitization keyword to the valid password field which will make sure the password gets transformed into a more secure hash.  \nFor custom keyword definition see `ajv`'s [official documentation](https://github.com/epoberezkin/ajv/blob/master/CUSTOM.md).  \nYou can then register the keyword to a `ajv` validator instance on your `serviser` [HttpApplication](https://lucid-services.github.io/serviser/App.html#getValidator) object.\n\n### about authentication/restricting access\n\nYou can define additional middlewares before or after data validation procedures.  \n\n- push a middleware to the top of middleware call stack that is run before input data are validated\n\n```javascript\nconst route = users.get('/:{key}')\nroute.step('auth', function auth(req, res) {\n    //do stuff\n});\nroute.validate(jsonSchema, 'query');\n```\n\n- push a middleware on top of middleware call stack, immediately after default input data validators\n\n```javascript\nconst route = users.get('/:{key}')\nroute.once('after-validation-setup', function() {\n    //at this point validation middlewares are already registered with the route\n    route.step('auth', function auth(req, res) {\n        //do stuff\n    });\n})\n```\n\n### request lifecycle events and implementing extra logic\nThese extra (asynchronous) events are available on [Route](https://lucid-services.github.io/serviser/Route.html) objects:\n\n- `after-validation-setup` - emitted once after all default input data validators are attached, when you attach additional middlewares inside event listener, you can be sure data are validated at that point\n- `bofore-query` - emitted once before the main sql query executed, the event is provided with `req` object and knex `query` object\n- `bofore-response` - emitted once before the response is sent, the user can override the response, the event is provided with `req` object \u0026 response data\n\nexample:\n\n```javascript\nconst route = users.get('/:{key}')\nroute.on('before-query', function(req, query) {\n    return query.where('banned', false);\n});\n```\n\n### Global configuration options\n\nSome validation constants of spetial query properties are configurable through config object:  \n\n```javascript\nconst Restfulness = require('serviser-restfulness');\n\nRestfulness.config.set('limit:maximum'); //query _limit maximum value, default 500\nRestfulness.config.set('limit:minimum'); //query _limit minimum value, default 0\nRestfulness.config.set('limit:default'); // query _limit default value when not provided by API call, default 0 meaning no limit at all\n\nRestfulness.config.set('offset:maximum'); //query _offset maximum value, default Number.MAX_SAFE_INTEGER\nRestfulness.config.set('offset:minimum'); //query _offset minimum value, default 0\nRestfulness.config.set('offset:default'); //query _offset default value, default 0\n\nRestfulness.config.set('embed:maxLength'); //default 256\nRestfulness.config.set('sort:maxLength'); //default 128\nRestfulness.config.set('filter:maxLength'); //default 32\nRestfulness.config.set('filter:minLength'); //default 0\nRestfulness.config.set('filter:minimum'); //default Number.MAX_SAFE_INTEGER\nRestfulness.config.set('filter:maximum'); //default Number.MIN_SAFE_INTEGER\nRestfulness.config.set('filter:maxItems'); //default 10\n```\n\nTests\n-------------------\n\n- unit tests  \n`npm test`\n\n- integration tests  \n`npm run test:docker`\n\n- development\n\n`npm run test:shell`  \n\nYou will be dropped in docker container shell where you can run:  \n\n```bash\n\u003e npm run migrate\n\u003e npm test\n\u003e npm run test:integration\n```\nYour project files on host systems will be mounted into the container's fs so you can edit and immediately test changes to the project files.  \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucid-services%2Fserviser-restfulness","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flucid-services%2Fserviser-restfulness","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucid-services%2Fserviser-restfulness/lists"}