{"id":19279488,"url":"https://github.com/eeditiones/roaster","last_synced_at":"2025-02-23T21:43:52.468Z","repository":{"id":40302332,"uuid":"289736994","full_name":"eeditiones/roaster","owner":"eeditiones","description":"Open API Router for eXist","archived":false,"fork":false,"pushed_at":"2024-10-16T09:56:00.000Z","size":1772,"stargazers_count":23,"open_issues_count":10,"forks_count":9,"subscribers_count":13,"default_branch":"main","last_synced_at":"2025-01-05T16:29:06.621Z","etag":null,"topics":["exist-db","library","router"],"latest_commit_sha":null,"homepage":"","language":"XQuery","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/eeditiones.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","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},"funding":{"github":"eeditiones","custom":"https://www.paypal.com/paypalme/eeditiones"}},"created_at":"2020-08-23T17:43:56.000Z","updated_at":"2024-10-16T09:56:03.000Z","dependencies_parsed_at":"2024-08-26T18:57:51.376Z","dependency_job_id":"52ad6091-6187-4219-a886-fe282f8d4bf0","html_url":"https://github.com/eeditiones/roaster","commit_stats":null,"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eeditiones%2Froaster","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eeditiones%2Froaster/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eeditiones%2Froaster/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eeditiones%2Froaster/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eeditiones","download_url":"https://codeload.github.com/eeditiones/roaster/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240385198,"owners_count":19792980,"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":["exist-db","library","router"],"created_at":"2024-11-09T21:15:23.277Z","updated_at":"2025-02-23T21:43:52.441Z","avatar_url":"https://github.com/eeditiones.png","language":"XQuery","funding_links":["https://github.com/sponsors/eeditiones","https://www.paypal.com/paypalme/eeditiones"],"categories":[],"sub_categories":[],"readme":"\n# Roaster\n\u003cimg alt=\"roaster router logo\" src=\"icon.svg\" width=\"128\" /\u003e\n\n\u003e **Define** your API, **then** implement it.\n\n![Test and Release](https://github.com/eeditiones/roaster/workflows/Test%20and%20Release/badge.svg) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)\n\n## OpenAPI Router for eXist\n\n\nRoaster is a generic router to be used in any exist-db application. It reads an [OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.0) specification from a JSON file and routes requests to handler functions written in XQuery.\n\n![Roasted API](doc/roasted-api.png)\n\nFrom any valid API specification you can generate an interactive documentation as the one above.\nThis is also very helpful for exploratory testing of your implementation (see [demo app](#demo-app)).\n\nRoaster is the routing library powering [TEI Publisher](https://teipublisher.com). So, make sure to also check there for additional [documentation](https://teipublisher.com/exist/apps/tei-publisher/doc/documentation.xml?odd=docbook.odd\u0026id=api) and examples.\n\n## Installation\n\nThis library XAR can be either downloaded from the [releases](https://github.com/eeditiones/roaster/releases) and is also available\non [eXist-db's public package repository](https://exist-db.org/exist/apps/public-repo/packages/roaster).\n\n## How it works\n\neXist applications usually have a controller as main entry point. The \n[controller.xql](test/app/controller.xql) in the example application only handles requests to static resources, but forwards all other requests to an XQuery script [api.xql](test/app/modules/api.xql). This script imports the OpenAPI router module and calls `roaster:route`, passing it one or more Open API specifications in JSON format.\n\nThe [demo app](#demo-app), included in this repository, uses two specifications:\n\n- [api.json](test/app/api.json)\n  demonstrates basic usage like parameters in path, query and body\n  as well as file up- and downloads\n- [api-jwt.json](test/app/api-jwt.json)\n  introduces more advanced use-cases like custom authentication and middlewares\n\nSplitting up your api specifications can help keeping each focussed on specific tasks and also split up really long ones.\n\nTEI Publisher has [api.json](https://github.com/eeditiones/tei-publisher-app/tree/master/modules/lib/api.json) and [custom-api.json](https://github.com/eeditiones/tei-publisher-app/tree/master/modules/custom-api.json). There, it is done to make it easier for users to extend the default API.\nIt is also possible to overwrite a route from `api.json` by placing it into `custom-api.json`.\n\nEach route in the specification _must_ have an `operationId` property.\nThis is the name of the XQuery function that will handle the request to the given route. Several routes _can_ use the same handler function, where applicable.\nThe XQuery function will be resolved by the lookup-function passed to  `roaster:route`. In order for that to work all route handler functions need to be available in the context of that function. This is why [api.xql](test/app/modules/api.xql) imports all modules containing handler functions.\n\n## Route Handling\n\nThe XQuery handler function _must_ expect exactly one argument: `$request as map(*)`. This is a map with a number of keys:\n\n* _id_: a uuid identifying this request (useful to find this exact request in your logfile)\n* _parameters_: a map containing all parameters (path and query) which were defined in the spec. The key is the name of the parameter, the value is the parameter value cast to the defined target type.\n* _body_: the body of the request (if ~requestBody~ was used), cast to the specified media type (currently application/json or application/xml).\n* _config_: the JSON object corresponding to the Open API path configuration for the current route and method\n* _user_: contains the authenticated user, if any authentication was successful\n* _method_: GET, POST, PUT, DELETE, HEAD...\n* _path_: the requested path\n* _spec_: the entire API definition this route is defined in\n\nFor example, here's a simple function which just echoes the passed in parameters:\n\n```xquery\ndeclare function custom:echo($request as map(*)) {\n    $request?parameters\n};\n```\n\n## Responses\n\nIf the function returns a value, it is sent to the client with a HTTP status code of 200 (OK). The returned value is converted into the specified target media type (if any, otherwise `application/xml` is assumed).\n\nTo modify responses like HTTP status code, body and headers the handler function may call `roaster:response` as its last operation.\n\n- `roaster:response($code as xs:int, $data as item()*)`\n- `roaster:response($code as xs:int, $mediaType as xs:string?, $data as item()*)`\n- `roaster:response($code as xs:int, $mediaType as xs:string?, $data as item()*, $headers as map(*)?)`\n\n**Example:**\n\n```xquery\ndeclare function custom:response($request as map(*)) {\n    roaster:response(427, \"application/octet-stream\", \"101010\", \n      map { \"x-special\": \"23\", \"Content-Length\" : \"1\" })\n};\n```\n\n## Error Handling\n\nIf an error is encountered when processing the request, a JSON record is returned.\n\nExample:\n\n```json\n{\n  \"module\": \"/db/apps/oas-test/modules/api.xql\",\n  \"code\": \"errors:NOT_FOUND_404\",\n  \"value\": \"error details\",\n  \"line\": 34,\n  \"column\": 5,\n  \"description\": \"document not found\"\n}\n```\n\nRequest handlers can also throw explicit errors using the variables defined in [errors.xql](content/errors.xql)\n\nExample:\n\n```xquery\nerror($errors:NOT_FOUND, \"HTML file \" || $path || \" not found\", map { \"info\": \"additional info\"})\n```\n\nThe server will respond with the HTTP status code 404 to the client.\nThe description and additional information will be added to the data that is sent.\n\nHowever, for some operations you may want to handle an error instead of just returning it to the client. In this case use the extension property `x-error-handler` inside an API path item. It should contain the name of an error handler function, which is expected to take exactly one argument, a map(*).\n\n## Authentication\n\n`basic` and `cookie` authentication are supported by default when the two-parameter signature of `roaster:router` is used.\nThe key based authentication type corresponds to eXist's persistent login mechanism and uses cookies for the key. To enable it, use the following securityScheme declaration:\n\n```json\n\"components\": {\n    \"securitySchemes\": {\n        \"cookieAuth\": {\n            \"type\": \"apiKey\",\n            \"name\": \"org.exist.login\",\n            \"in\": \"cookie\"\n        }\n    }\n}\n```\n\nThe security scheme must be named **cookieAuth**. The `name` property defines the login session name to be used.\n\nCustom authentication strategies are possible. The test application has an example for JSON Web Tokens.\n\n## Access Constraints\n\nCertain operations may be restricted to defined users or groups. We use an implementation-specific property, 'x-constraints' for this on the operation level, e.g.:\n\n```json\n\"/api/upload/{collection}\": {\n  \"post\": {\n    \"summary\": \"Upload a number of files\",\n    \"x-constraints\": {\n        \"groups\": \"dba\"\n    }\n  }\n}\n```\n\nrequires that the *effective user* or *real user* running the operation belongs to the \"tei\" group.\nThe *effective user* will be used, if present.\n\n**groups** can be an array, too. In that case the user must be in at least one of them.\n\n```json\n{ \"groups\": [\"tei\", \"dba\"] }\n```\n\nThis will work also for custom authorization strategies. The handler function needs to [extend the request map with the user information](test/app/modules/jwt-auth.xqm#L66).\n\n## Middleware\n\nIf you need to perform certain actions on each request you can add a transformation function also known as middleware.\n\nMost internal operations that construct the $request map passed to your operations are such functions. Authorization is a middleware as well.\n\nA middleware has two parameters of type map, the current request map and the current response, and returns two map that will become the request and response maps for the next transformation.\n\nExample middleware that adds a \"beep\" property to each request and a custom x-beep header to each response:\n\n```xquery\ndeclare function custom-router:use-beep-boop ($request as map(*), $response as map(*)) as map(*) {\n    (: extend request :)\n    map:put($request, \"beep\", \"boop\"),\n    (: add custom header to all responses :)\n    map:put($response, $router:RESPONSE_HEADERS, map:merge((\n      $response?($router:RESPONSE_HEADERS),\n      map { \"x-beep\": \"boop\" }\n    ))\n};\n```\n\n## File Uploads\n\nRoaster transparently handles data from multipart/form-data requests to keep route handlers short and readable.\nPlease see the [file upload documentation](doc/file-upload.md) for more details on this.\n\n## Limitations\n\nThe library does not support yet support following OpenAPI feature(s): \n\n- `$ref` references in the Open API specification ([issue](https://github.com/eeditiones/roaster/issues/39))\n\n## Development\n\nClone this repository and switch to your local working directory.\n\n### Requirements\n\n-  [node](https://nodejs.org/en/): `v14+`\n-  [exist-db](https://www.exist-db.org): `v5.0.0+`\n-  [Ant](https://ant.apache.org): `v1.10.9+` (optional)\n\n### Building and Installation\n\nRoaster uses Gulp as its build tool which itself builds on NPM. \nTo initialize the project and load dependencies run\n\n```bash\nnpm i\n```\n\n\u003e Note: the `install` commands below assume that you have a local eXist-db running on port 8080. However the database connection can be modified in .existdb.json.\n\n| Run | Description |\n|---------|-------------|\n|```gulp build```|to just build the roaster routing lib. |\n|```gulp build:all```|to build the routing lib and the demo app.|\n|```gulp install```|To build and install the lib in one go|\n|```gulp install:all```|To build and install lib and demo app run|\n\nThe resulting xar(s) are found in the root of the project.\n\nAn ant-task is still defined, but will use gulp in the end (through `npm run build`).\n\n### Demo App\n\nThe repository contains a demo and test application, 'Roasted', which is using the Roaster router. It serves a good starting-point for playing, learning and as a 'template' for your own apps.\n\nThe demo app is now available for download as an artefact of a [release](https://github.com/eeditiones/roaster/releases/latest).\n\n1. download the **roasted.xar**\n2. install it in your eXist-db instance\n\n    You can use the **upload** feature of the dashboard. \n    The roaster library will be installed as a dependency in the required version.\n\n1. open http://localhost:8080/exist/apps/roasted/\n\n    If your instance is running on a different domain replace `localhost:8080` with the correct one.\n    This will open a form dynamically created from the definition files [api.json](test/app/api.json) _and_ [api-jwt.json](test/app/api-jwt.json).\n\n#### Building the demo application from source\n\nIf you want to make modifications to the demo app and test them\n\n1. `gulp install:all`\n\n    will create both the library and the testapp XAR and install them.\n\n1. open http://localhost:8080/exist/apps/roasted/\n\n    This will open a form dynamically created from the definition files [api.json](test/app/api.json) _and_ [api-jwt.json](test/app/api-jwt.json).\n\n### Development\n\nRunning `gulp watch` will build and install the library and watch\nfor file changes. Whenever one of the watched files is changed a \nfresh version of the xar will be installed in the database.\nThis included the test application in `test/app`.\n\n### Testing\n\nTo run the local test suite you need an instance of eXist running on `localhost:8080` and `npm` to be available in your path. To test against a different different server, or use a different user or password you can copy `.env.example` to `.env` and edit it to your needs.\n\nRun the test suite with\n\n```shell\nnpm test\n```\n\nAdditional tests that cover this package are contained in the [tei-publisher-app](https://github.com/eeditiones/tei-publisher-app) repository.\n\n## Contributing\n\nRoaster uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) to determine semantic versioning of releases, see these examples:\n\n| Commit message  | Release type |\n|-----------------|--------------|\n| `fix(pencil): stop graphite breaking when too much pressure applied` | Patch Release |\n| `feat(pencil): add 'graphiteWidth' option` | ~~Minor~~ Feature Release |\n| `perf(pencil): remove graphiteWidth option`\u003cbr/\u003e\u003cbr/\u003e`BREAKING CHANGE: The graphiteWidth option has been removed.`\u003cbr/\u003e`The default graphite width of 10mm is always used for performance reasons.` | ~~Major~~ Breaking Release |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feeditiones%2Froaster","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feeditiones%2Froaster","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feeditiones%2Froaster/lists"}