{"id":21082238,"url":"https://github.com/olsonpm/sqlite-to-rest","last_synced_at":"2025-10-25T12:51:02.826Z","repository":{"id":80606783,"uuid":"61088212","full_name":"olsonpm/sqlite-to-rest","owner":"olsonpm","description":null,"archived":false,"fork":false,"pushed_at":"2024-09-26T22:21:37.000Z","size":1759,"stargazers_count":391,"open_issues_count":0,"forks_count":20,"subscribers_count":6,"default_branch":"dev","last_synced_at":"2025-03-27T05:09:10.022Z","etag":null,"topics":["automatic-api"],"latest_commit_sha":null,"homepage":null,"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/olsonpm.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-06-14T03:14:26.000Z","updated_at":"2024-11-06T06:58:52.000Z","dependencies_parsed_at":null,"dependency_job_id":"a9814aba-e29c-4245-a3c8-ce6f183e85e6","html_url":"https://github.com/olsonpm/sqlite-to-rest","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olsonpm%2Fsqlite-to-rest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olsonpm%2Fsqlite-to-rest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olsonpm%2Fsqlite-to-rest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olsonpm%2Fsqlite-to-rest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/olsonpm","download_url":"https://codeload.github.com/olsonpm/sqlite-to-rest/tar.gz/refs/heads/dev","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247242683,"owners_count":20907134,"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":["automatic-api"],"created_at":"2024-11-19T20:13:12.683Z","updated_at":"2025-10-25T12:50:57.753Z","avatar_url":"https://github.com/olsonpm.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sqlite To Rest\n\nKoa routing middleware allowing you to expose a sqlite database via RESTful CRUD\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n## Table of Contents\n\n- [Why build it?](#why-build-it)\n- [Features](#features)\n- [Limitations](#limitations)\n- [Tutorial](#tutorial)\n- [CLI](#cli)\n- [API](#api)\n- [RESTful CRUD Operations](#restful-crud-operations)\n- [Reference](#reference)\n- [Test](#test)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Why build it?\n\nMostly because I wanted to dig deeper into node web server code, but also\nbecause I haven't jumped onto the NoSQL bandwagon and think that web APIs are\nextremely useful. The result is a modest attempt at automating the CRUD\nboilerplate that every developer hates, while following the specs to make API\nconsumption intuitive. I chose sqlite to keep the database side of things\nsimple, with the intent that the API isn't serving heavy loads.\n\n## Features\n\n- Spec compliant CRUD RESTful API to an existing database's tables and views\n- GET utilizes [JSONStream](https://github.com/dominictarr/JSONStream) so the\n  entire response is not held in memory, allowing for arbitrarily\n  large responses.\n- Range requests with the custom range unit 'rows' can be used to GET specific\n  rows. While compliant with rfc7233, the syntax and semantics were kept\n  extremely similar to byte-ranges.\n- The server can configure a maximum request range per table since the amount\n  of data per row will vary per-table.\n- The server can also configure whether to send the content-range header\n  in a HEAD request. This allows the author to save the server from\n  unnecessarily calculating the count on a table that is known to be\n  very large.\n- Custom request header 'order' and conditional response header 'accept-order'\n  exposes row sorting by column with optional ascending and\n  descending specifiers\n- The API enforces correct usage, while sending developer-friendly error\n  messages upon 4xx errors.\n- Comes with a friendly CLI to create a bare-bones koa server to get you up and\n  running quickly.\n\n## Limitations\n\n- All tables must use primary keys. The next limitation explains why.\n- In effort to mitigate damage, unsafe methods only allow modification of\n  single rows. This is enforced by matching the query parameters with the\n  'primary key' columns - friendly errors will tell you if called called an\n  unsafe method incorrectly.\n- No built-in API key management. The library as-is can only serve\n  trusted consumers.\n- It's sqlite. This library is not meant for clustering or large workloads.\n  See ['Situations Where A Client/Server RDBMS May Work\n  Better'](https://www.sqlite.org/whentouse.html) for details.\n- This is my foray into reading rfc's and working with web server libraries\n  (koa and middleware in general). I have tests and feel confident in my\n  comprehension of the concepts, but the code is not the prettiest.\n- No friendly data validation currently. Right now contextless 500 statuses\n  are returned if data doesn't pass constraints, and I don't have tests\n  ensuring consistent behavior around data validation.\n\n## Tutorial\n\n[This tutorial](https://github.com/olsonpm/sqlite-to-rest/blob/dev/docs/tutorial.md)\nwill walk you through\n\n1. Creating an initial database\n2. Using sqlite-to-rest's CLI to create a bare-bones koa server\n3. Walk you through some curl commands to test the server's CRUD RESTful api.\n\n## CLI\n\nBy installing this library globally, you receive access to `sqlite-to-rest`.\n\nThe CLI currently contains one command `generate-skeleton` which creates an\ninitial bare-bones koa server from an existing sqlite database. This should\nhelp you get started.\n\nSee `sqlite-to-rest --help` for more info.\n\n## API\n\n`require('sqlite-to-rest')` returns an object with two properties.\n\n- **generateSkeleton**: [madonna-function](https://github.com/olsonpm/madonna-function)\n  -\u003e promise(undefined)  \n  This will usually be called from the CLI but is also made\n  available via the js API. Its purpose is to generate a barebones koa\n  server to get you up and running. In the directory it will:\n\n  1.  Run `npm init -f` if a package.json doesn't exist\n  2.  Output the generated koa server named 'skeleton.js'\n  3.  Install and save the dependencies required to run the server\n\n  It takes two properties\n\n  - **dir**: [`isLadenString`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)\n    [`isDirectory`](#isdirectory)  \n    Directory to generate the koa server.\n\n  - **dbPath** _optional_: [`isSqliteFile`](#issqlitefile)  \n    Path to your sqlite3 database.\n\n  ```js\n  // example\n  sqliteToRest\n    .generateSkeleton({\n      dir: beerApiDir,\n      dbPath: 'path/to/your/db.sqlite3',\n    })\n    .then(() =\u003e {\n      /* skeleton.js is ready to be ran */\n    })\n  ```\n\n- **getSqliteRouter**: [madonna-function](https://github.com/olsonpm/madonna-function)\n  -\u003e promise([koa-router](https://github.com/alexmingoia/koa-router/tree/master))  \n  This function generates the RESTful CRUD routing and returns the modified\n  koa-router instance.\n\n  It takes two properties\n\n  - **dbPath**: [`isSqliteFile`](#issqlitefile)  \n    Path to your sqlite3 database.\n\n  - **config** _optional_: A [routing config object](#routing-config-object)\n\n  ```js\n  // example\n  const app = new require('koa')(),\n    dbPath = 'path/to/your/db.sqlite3'\n\n  getSqliteRouter({ dbPath }).then((router) =\u003e {\n    app.use(router.routes())\n    // ...\n  })\n  ```\n\n## RESTful CRUD Operations\n\nThe following is a list of the available crud operations made available by the\nRESTful API in the form of pseudo examples. All assume a beer table with two\ncolumns `id INTEGER PRIMARY KEY` and `name` which is nullable.\n\nAs noted in [limitations](#limitations), Be aware that unsafe methods (DELETE\nand POST) can only affect one row at a time.\n\n- GET\n  This allows for the most variation. [Click here](#get-query-operators) for\n  all available query operators. Keep in mind the following examples ignore\n  proper [query encoding](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)\n\n  Headers may be specified right below urls\n\n  - **/beer**  \n    Requests for all rows\n\n  - **/beer?id=1**  \n    Where id = 1\n\n  - **/beer**  \n    `range: rows=0-2`  \n    First three rows\n\n  - **/beer**  \n    `range: rows=-5`  \n    Last five rows\n\n  - **/beer**  \n    `range: rows=0-`  \n    As many rows as the server is able to provide, which in practice will be\n    the smaller of [`maxRange`](#tabular-config-object) and total row count.\n\n  - **/beer**  \n    `range: rows=1-`  \n    As many rows as the server is able to provide, starting from row 1.\n\n  - **/beer**  \n    `order: name`  \n    Ordered by name ascending\n\n  - **/beer**  \n    `order: name desc`  \n    Ordered by name descending\n\n  - **/beer**  \n    `order: name desc,id`  \n    Contrived, but orders first by name descending, and in the case of a tie\n    by id ascending.\n\n  - **/beer?id\u003e1**  \n    Where id \u003e 1\n\n  - **/beer?id\u003e=2\u0026id\u003c5**  \n    Where id \u003e= 2 and id \u003c 5\n\n  - **/beer?name_NOTNULL**  \n    Where name is not null\n\n  - **/beer?name_ISNULL**  \n    Where name is null\n\n  - **/beer?id!=5\u0026name_LIKE'Spotted%'**  \n    Where id != 5 and name is LIKE \"Spotted%\" (ignore quotes)\n\n  - **/beer?id\u003e=1\u0026id\u003c10\u0026name_LIKE'Avery%'**  \n    `order: name desc,id` `range: rows=2-4`  \n    Contrived for sake of example.  \n    Get beer with ids between 1 and 9 inclusive, with name like \"Avery%\",\n    ordered first by name descending then by id ascending, getting the third\n    through 5th rows of the result. Or in SQL:\n\n    ```sql\n    SELECT *\n    FROM beer\n    WHERE id \u003e= 1\n      AND id \u003c10\n      AND name LIKE 'Avery%'\n    ORDER BY name desc, id\n    LIMIT 3 OFFSET 2\n    ```\n\n- DELETE\n  Requires a query string with all primary keys set equal to a value. This\n  enforces a maximum deletion of a single row.\n\n  - **/beer?id=1**  \n    Deletes beer with id=1\n\n  _if the beer table instead had a composite primary key of both id and name_\n\n  - **/beer?id=1\u0026name='Avery IPA'**\n\n* POST create  \n  Must not pass a query string. If a query string is passed, then POST update\n  is assumed. All POST requests must pass the header\n  `content-type: application/json`.\n\n  Keep in mind the body must contain all non-nullable and non INTEGER\n  PRIMARY KEY columns. A 400 response will be sent otherwise indicating what\n  fields were missed. Nullable columns will default to null and INTEGER\n  PRIMARY KEY columns will automatically increment per\n  [sqlite3 specifications](https://www.sqlite.org/autoinc.html).\n\n  Json data will be specified right below urls\n\n  - **/beer**  \n    `{\"id\":1,\"name\":\"Serendipity\"}`  \n    Creates a beer with id = 1 and name = 'Serendipity'\n\n  - **/beer**  \n    `{\"id\":1}`  \n    Creates a beer with id = 1 and name = NULL\n\n  - **/beer**  \n    `{\"name\":\"Serendipity\"}`  \n    Creates a beer with id set to the next incremented value per\n    [sqlite3 INTEGER PRIMARY KEY specifications](https://www.sqlite.org/autoinc.html)\n\n  - **/beer**  \n    `{}`  \n    Creates a beer with id incremented, and name set to NULL\n\n* POST update  \n  Must contain a query string. Without a query string, POST create is assumed.\n  As with POST create, the header `content-type: application/json`\n  is mandatory.\n\n  The query string must contain all primary keys to ensure only a single row\n  gets updated. If incorrect values are passed, a 400 will be returned\n  listing the offending keys.\n\n  The request body must contain a non-empty object and must contain valid\n  keys corresponding to column names.\n\n  Json data will be specified right below urls\n\n  - **/beer?id=1**  \n    `{\"id\":2}`  \n    Updates beer with id of 1 setting it to two.\n\n  - **/beer?id=1**  \n    `{\"name\":\"Two Women\"}`  \n    Updates beer with id of 1 setting its name to Two Women.\n\n  _if the beer table instead had a composite primary key of both id and name_\n\n  - **/beer?id=1\u0026name=Two Women**  \n    `{\"name\":\"Moon Man\"}`  \n    Updates beer where id is one and name is Two Women, setting name to Moon Man\n\n## Reference\n\n#### isSqliteFile\n\n- Just checks the first 16 bytes of the file to see if it equals\n  'sqlite format 3' followed by a null byte.\n\n#### isDirectory\n\n- Returns the result of [fs.statsSync](https://nodejs.org/api/fs.html#fs_fs_statsync_path)\n  followed by [\u003cStats\u003e.isDirectory](https://nodejs.org/api/fs.html#fs_class_fs_stats)\n\n#### isFile\n\n- Returns the result of [fs.statsSync](https://nodejs.org/api/fs.html#fs_fs_statsync_path)\n  followed by [\u003cStats\u003e.isFile](https://nodejs.org/api/fs.html#fs_class_fs_stats)\n\n### GET query operators\n\nQuery conditions must be delimited by ampersands e.g. `id\u003e5\u0026name!=Spotted Cow`\n\nBinary operators (require a value after)  \n**=**  \n**!=**  \n**\\\u003e=**  \n**\u003c=**  \n**\\\u003e**  \n**\u003c**  \n**\\_LIKE**\n\n- **\\_LIKE** is special in that it must have opening and closing single\n  quotes. If not, a 400 error will be thrown showing where the parsing was\n  unable to complete and what was expected. See [RESTful CRUD Operations](#restful-crud-operations) for examples.\n\nUnary operators (must follow a column name)  \n**\\_ISNULL**  \n**\\_NOTNULL**\n\n#### Router config object\n\n[`isLadenPlainObject`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)  \nThe purpose of this object is to provide generic configuration for the sqlite\nrouter. The following properties are allowed:\n\n- **prefix**: [`isLadenString`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)\n  The string passed to [`koa-router's`](https://github.com/alexmingoia/koa-router/tree/master)\n  [`prefix`](https://github.com/alexmingoia/koa-router/tree/master#new-routeropts)\n  constructor option. For example, the skeleton server doesn't specify\n  a prefix, allowing the beer api to be hit directly from the domain root\n  `http://localhost:8085/beer`. If you set prefix to '/api', then you\n  would instead send requests to `http://localhost:8085/api/beer`.\n\n- **allTablesAndViews**: A [tabular configuration object](#tabular-config-object)  \n  The configurations specified in this object will apply for all tables and\n  views, optionally overridden by the `tablesAndViews` property.\n\n- **tablesAndViews**: [`isLadenPlainObject`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)\n  The object passed **must** have keys matching the database column or view\n  names. If not, a friendly error message will be thrown. The values for each\n  table and view must be a [tabular configuration object](#tabular-config-object)\n\n#### Tabular config object\n\n[`isLadenPlainObject`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)\nThis object represents configurations that can be set for either views\nor tables. It allows the following properties:\n\n- **maxRange**: [`isPositiveNumber`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)  \n  _Application default_: 1000  \n  This is the maximum range your server will allow requests for. If a GET\n  request comes in with no range header, the spec assumes they want the entire\n  resource. If the number of rows resulting in that GET is greater than\n  maxRange, then a 416 status is returned with the custom header\n  [`max-range`](#custom-headers). The application default is purposefully\n  conservative in hopes that authors will set maxRange according to\n  their needs.  \n  _Note that 'Infinity' is a valid positive number._\n\n- **flags**: [`isLadenArray`](https://github.com/olsonpm/madonna-fp#custom-to-this-library)  \n  Currently the only flag accepted is the string 'sendContentRangeInHEAD'.\n  When set, HEAD requests will return the available content range in the form\n  `content-range: */\u003cmax-range\u003e`. The reason it's configurable is that\n  calculating max-range may be more work than its worth, depending on the load\n  of the server and the size of your tables.\n\n### Custom Headers\n\n#### Request\n\n- **order**: This header is only defined for GET, and can be thought of as\n  the sql ORDER BY equivalent. It must contain a comma-delimited column names,\n  each optionally followed by a space and the strings 'asc' or 'desc'. If\n  incorrect order values are sent, a 400 response will indicate which ones.\n\n#### Response\n\nThese aren't all necessarily custom, but all their usage falls outside what's\ndefined in the spec and thus need clarification.\n\n- **GET**\n\n  - **max-range**: This header is returned when the requested number of rows\n    surpasses the configured [`maxRange`](#tabular-config-object). Note the\n    request might not specify the range header, but the number of rows\n    resulting in that resource will still be checked.\n\n  - **content-range**: [rfc7233](https://tools.ietf.org/html/rfc7233#section-4.2)\n    states\n\n    \u003e only the 206 (Partial Content) and 416 (Range Not Satisfiable) status\n    \u003e codes describe a meaning for Content-Range.\n\n    When sqlite-to-rest responds with a 200 status code, the content-range\n    header is sent with the 206 format of `\u003crow start\u003e-\u003crow end\u003e/\u003crow count\u003e`.\n\n    When a request is sent without a range header and the number of resulting\n    rows surpasses [`maxRange`](#tabular-config-object), a 400 is returned\n    with content-range set in the 416 format of `*/\u003crow count\u003e`\n\n    Note this header may be returned in a HEAD response.\n\n  - **accept-order**: This will be returned if the request header `order`\n    had bad syntax or specified incorrect column names. For details, refer\n    to HEAD -\u003e accept-order below.\n\n- **HEAD**\n\n  - **accept-order**: `accept-order` is just a comma-delimited list of the\n    requested table columns, intended to tell the client the valid columns\n    able to be used in the request header `order`.\n\n  - **max-range**: The configured [`maxRange`](#tabular-config-object)\n    of the requested table.\n\n  - **content-range**: This header will only be sent if the table has been\n    configured with the flag [`sendContentRangeInHEAD`](#tabular-config-object).\n    In that case, content-range is set to the 416 format of `*/\u003crow count\u003e`\n\n## Test\n\n`npm test`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folsonpm%2Fsqlite-to-rest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Folsonpm%2Fsqlite-to-rest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folsonpm%2Fsqlite-to-rest/lists"}