{"id":16777941,"url":"https://github.com/dgf/pgx-rest","last_synced_at":"2026-01-03T21:04:44.974Z","repository":{"id":19341803,"uuid":"22580844","full_name":"dgf/pgx-rest","owner":"dgf","description":"REST JSON endpoints on PostgreSQL","archived":false,"fork":false,"pushed_at":"2016-02-28T10:22:41.000Z","size":77,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-23T05:44:51.171Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PLpgSQL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dgf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-08-03T18:28:11.000Z","updated_at":"2019-07-15T21:20:37.000Z","dependencies_parsed_at":"2022-08-28T03:21:21.867Z","dependency_job_id":null,"html_url":"https://github.com/dgf/pgx-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/dgf%2Fpgx-rest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgf%2Fpgx-rest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgf%2Fpgx-rest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgf%2Fpgx-rest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dgf","download_url":"https://codeload.github.com/dgf/pgx-rest/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243918631,"owners_count":20368745,"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":[],"created_at":"2024-10-13T07:26:21.494Z","updated_at":"2026-01-03T21:04:44.902Z","avatar_url":"https://github.com/dgf.png","language":"PLpgSQL","funding_links":[],"categories":[],"sub_categories":[],"readme":"# REST on PostgreSQL\n\nTest implementation of REST compatible endpoints with PostgreSQL functions.\n\nTo start a ROCA based application, with a backend like this, you only need an\nenvironment with a HTTP server and a PostgreSQL interface - that's it!\n\nRequirements: only PostgreSQL 9.4 or higher\n\n## Installation\n\nTo install [PostgreSQL][postgres] on Ubuntu or Debian follow these [instructions][pg_apt]\nand use a tagged repository to overrule the libpg version.\n\n    deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main 9.4\n\n## Setup\n\nDefine a database name in the Makefile or use `rest_check` (the default).\n\ncreate a fresh database and load required contrib modules\n```sh\n$ make init\n$ make pg-setup\n```\n\ncreate example application\n```sh\n$ make application\n```\n\nlist all existing targets\n```sh\n$ make\ntargets:                # list all targets\ninit:                   # create database\ncli:                    # connect Postgres terminal\nng-download:            # fetch and unpack Nginx\nng-compile: ng-download # compile Nginx\nng-install: ng-compile  # install Nginx\nng-run:                 # start Nginx\npg-setup:               # setup Postgres extensions\nclean:                  # clean database\nrest: clean             # install REST schema\ntest: application       # run specifications\n```\n\n## Usage\n\nThe example application contains a rudimentary task and contact management.\n\nlist all existing routes\n```SQL\nSELECT method, path, proc, legitimate, description FROM route ORDER by path, method;\n method |          path          |         proc          | legitimate |             description              \n--------+------------------------+-----------------------+------------+--------------------------------------\n get    | /                      | homepage              | {every}    | index page\n post   | /contact               | post_contact          | {every}    | create a contact\n get    | /contact/{id}          | get_contact           | {every}    | contact details\n put    | /contact/{id}          | put_contact           | {every}    | update contact details\n delete | /contact/{id}          | delete_contact        | {every}    | delete a contact\n put    | /contact/{id}/address  | put_contact_address   | {every}    | update contact address\n get    | /contacts              | get_contacts          | {every}    | contact list\n get    | /file                  | form_upload           | {every}    | file upload form\n get    | /file/{id}             | get_file              | {every}    | file details page\n put    | /file/{id}             | put_file              | {every}    | update file meta data\n delete | /file/{id}             | delete_file           | {every}    | delete a file\n get    | /file/{id}/delete      | form_delete_file      | {every}    | confirm file delete\n get    | /files                 | get_files             | {every}    | file list\n get    | /login                 | form_login            | {every}    | login form\n get    | /routes                | get_routes            | {admin}    | list all published routes\n get    | /task                  | form_post_task        | {every}    | template route of task creation form\n post   | /task                  | post_task             | {every}    | create a task\n get    | /task/{id}             | get_task              | {every}    | get task details\n put    | /task/{id}             | put_task              | {user}     | update a task\n delete | /task/{id}             | delete_task           | {user}     | delete a task\n get    | /task/{id}/cancel      | form_post_task_cancel | {user}     | confirm task cancel\n post   | /task/{id}/cancel      | post_task_cancel      | {user}     | cancel a task\n get    | /task/{id}/delete      | form_delete_task      | {user}     | confirm task delete\n get    | /task/{id}/finish      | form_post_task_finish | {user}     | confirm task finish\n post   | /task/{id}/finish      | post_task_finish      | {user}     | finish a task\n get    | /task/{id}/reopen      | form_post_task_reopen | {user}     | confirm task reopen\n post   | /task/{id}/reopen      | post_task_reopen      | {user}     | reopen a task\n get    | /tasks                 | get_tasks             | {every}    | all tasks\n get    | /tasks?status={status} | get_tasks             | {every}    | filter tasks\n get    | /templates             | get_templates         | {admin}    | list all published templates\n```\n\ncreate a new task\n```SQL\nSELECT data FROM post('/task', '{\"subject\": \"todo\", \"description\": \"something\"}'::json);\n{\"id\" : 6, \"status\" : \"open\", \"subject\" : \"todo\", \"description\" : \"something\", \"routes\" : {\"get\" : \"/task/6\", \"delete\" : \"/task/6\", \"put\" : \"/task/6\", \"cancel\" : \"/task/6/cancel\", \"finish\" : \"/task/6/finish\"}}\n(1 row)\n```\n\n## Public API routes\n\n * generic route execution: ```SELECT * FROM call('get', '/tasks', NULL, NULL);```\n * template resolver: ```SELECT * FROM find_template('html', '/tasks');```\n * HTTP basic login: ```SELECT * FROM login('icke', encode(concat_ws(':', 'icke', 'secret')::bytea, 'base64'));```\n * HTML form POST login: ```SELECT * FROM post_login('icke', 'secret');```\n * HTTP logout POST route: ```SELECT * FROM post_logout(sid);```\n\n### Public file module access\n\n * HTML form POST upload: ```SELECT * FROM upload(sid, 'a.txt', 'text/plain', 'a file', 'text data')```\n * HTTP download GET route: ```SELECT * FROM download(sid, 1)```\n\n## Development\n\ndefine a schema and insert some entities\n```SQL\nCREATE TABLE entity (\n  id   serial PRIMARY KEY,\n  name text   NOT NULL\n);\nINSERT INTO entity (name)\nVALUES ('first entity'), ('another one');\n```\n\ncreate JSON endpoint function\n```SQL\nCREATE FUNCTION get_entities(req request)\n  RETURNS response AS $$ DECLARE l json;\n  BEGIN\n    SELECT json_agg(e) FROM entity e INTO l;\n    RETURN (200, l);\n  END;\n$$ LANGUAGE plpgsql;\n```\n\nroute the endpoint\n```SQL\nINSERT INTO route (method, path, proc, legitimate, description)\nVALUES ('get', '/entities', 'get_entities', '{\"every\"}', 'entity list');\n```\n\ntest the route\n```SQL\nSELECT * FROM get('/entities');\n code |               data                \n------+-----------------------------------\n  200 | [{\"id\":1,\"name\":\"first entity\"}, +\n      |  {\"id\":2,\"name\":\"another one\"}]\n```\n\n## HTTP endpoints with OpenResty\n\nDownload, build and install an [OpenResty][openresty] release.\n```sh\n$ make ng-install\n```\n\ncreate an application user with login\n```SQL\nCREATE USER application WITH NOINHERIT ENCRYPTED PASSWORD 'SecreT';\nALTER USER application SET search_path = application, contacts, tasks, files, rest, public;\n```\n\nadjust credentials in `nginx.conf`\n```sh\n$ grep postgres_server nginx.conf\n    postgres_server 127.0.0.1 dbname=rest_check user=application password=SecreT;\n```\n\nstart Nginx\n```sh\n$ make ng-run\n``` \n\na simple task flow example session\n```sh\n# create a task\n$ curl -D - -H \"Content-Type: application/json\" -d '{\"subject\":\"todo\", \"description\":\"something\"}' http://localhost:8080/task\nHTTP/1.1 201 Created\nServer: openresty/1.7.2.1\nDate: Mon, 06 Oct 2014 20:40:04 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nSet-Cookie: session=NULL; Path=/; Expires=Mon, 06-Oct-14 20:40:04 GMT\n\n{\"id\" : 1, \"status\" : \"open\", \"subject\" : \"todo\", \"description\" : \"something\", \"routes\" : {\"get\" : \"/task/1\", \"delete\" : \"/task/1\", \"put\" : \"/task/1\", \"cancel\" : \"/task/1/cancel\", \"finish\" : \"/task/1/finish\"}}%\n\n# list all open tasks\n$ curl -D - http://localhost:8080/tasks\\?status\\=open\nHTTP/1.1 200 OK\nServer: openresty/1.7.2.1\nDate: Mon, 06 Oct 2014 20:40:41 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nSet-Cookie: session=NULL; Path=/; Expires=Mon, 06-Oct-14 20:40:41 GMT\n\n{\"tasks\" : [{\"id\" : 1, \"status\" : \"open\", \"subject\" : \"todo\", \"description\" : \"something\", \"routes\" : {\"get\" : \"/task/1\", \"delete\" : \"/task/1\", \"put\" : \"/task/1\", \"cancel\" : \"/task/1/cancel\", \"finish\" : \"/task/1/finish\"}}], \"routes\" : {\"post\" : \"/task\"}}%\n\n# finish the task (authenticated)\n$ curl -u er -D - -X POST http://localhost:8080/task/1/finish\nEnter host password for user 'er':\nHTTP/1.1 200 OK\nServer: openresty/1.7.2.1\nDate: Mon, 06 Oct 2014 20:41:43 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nSet-Cookie: session=4d2b96d8-2e53-42f6-a977-273fc42aa4fd; Path=/; Expires=Mon, 06-Oct-14 23:18:43 GMT\n\n{\"id\" : 1, \"status\" : \"done\", \"subject\" : \"todo\", \"description\" : \"something\", \"routes\" : {\"get\" : \"/task/1\", \"delete\" : \"/task/1\", \"put\" : \"/task/1\", \"reopen\" : \"/task/1/reopen\"}}%\n\n# there are no open tasks\n$ curl -D - http://localhost:8080/tasks\\?status\\=open\nHTTP/1.1 200 OK\nServer: openresty/1.7.2.1\nDate: Mon, 06 Oct 2014 20:42:27 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nSet-Cookie: session=NULL; Path=/; Expires=Mon, 06-Oct-14 20:42:27 GMT\n\n{\"tasks\" : [], \"routes\" : {\"post\" : \"/task\"}}%\n\n# POST a x-www-form-urlencoded task\n$ curl -D - -d 'subject=todo2\u0026description=something2' http://localhost:8080/task\nHTTP/1.1 201 Created\nServer: openresty/1.7.2.1\nDate: Mon, 06 Oct 2014 20:42:47 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nSet-Cookie: session=NULL; Path=/; Expires=Mon, 06-Oct-14 20:42:47 GMT\n\n{\"id\" : 2, \"status\" : \"open\", \"subject\" : \"todo2\", \"description\" : \"something2\", \"routes\" : {\"get\" : \"/task/2\", \"delete\" : \"/task/2\", \"put\" : \"/task/2\", \"cancel\" : \"/task/2/cancel\", \"finish\" : \"/task/2/finish\"}}%\n```\n\n### Authentication\n\nSupports HTTP basic auth and HTML form based authentication.\n\n#### HTTP Basic Auth\n\nThe way to communicate with a HTTP command client or library.\n\nFlow:\n 1. call of a restricted route, returns 401\n 2. call it again with HTTP Basic Auth, results in:\n   * 200 with route response and session cookie (logged in)\n   * 400 invalid login call\n   * 403 forbidden (not legitimated)\n 3. reuse session cookie for additional requests (don't reauthenticate every request)\n 4. post a /logout with cookie to invalidate the session\n\n#### HTML Form Auth\n\nTypical Browser interaction uses a form based dialog to prevent the hassle of\ninvalidating a HTTP basis auth session without JavaScript XHR.\n\nFlow:\n 1. call of a restricted route, returns 401\n 2. get /login page and post /authenticate, returns\n   * 200 JSON notice and session cookie (logged in)\n   * 303 HTTP redirect back and session cookie (logged in)\n   * 400 invalid login call\n   * 403 forbidden (not legitimated)\n 3. Browser reuses session cookie for additional requests\n 4. post a /logout to invalidate the session\n\n### Authorization\n\nroute execution is restricted with a legitimated set of roles\n\nroles type with special entry \"every\" for public resource access\n\n### Templates\n\nrequires [lua-resty-template][lua-resty-template], the simplest way to install it is `luarocks`\n```sh\n$ sudo luarocks install lua-resty-template\n```\n\nlist all template mappings\n```SQL\nSELECT proc, mime, path, locals FROM template;\n       proc       | mime |        path        |          locals\n------------------+------+--------------------+---------------------------\n get_task         | html | tasks/details.html | {\"title\":\"task details\"}\n get_tasks        | html | tasks/index.html   | {\"title\":\"task list\"}\n get_tasks        | svg  | tasks/stats.svg    | {\"title\":\"task stats\"}\n create_task_form | html | tasks/create.html  | {\"title\":\"create a task\"}\n(4 rows)\n```\n\n### Architecture\n\ninternal route proccessing\n\n 1. NG get cookie session ID\n 2. NG login with HTTP basic auth \u003e PG login()\n 3. PG update session\n 4. PG authorized call\n 5. NG handle response (render template)\n\n\n### HTML HTTP REST Hacks for ROCA\n\nThe lack of HTTP methods in HTML should have no effects on the REST interface.\nTo minify the impact on the business layer there some default behaviours implemented\nfor `application/x-www-form-urlencoded` requests.\n\n#### POST HTML form HTTP redirect\n\n201 POST response is rewritten to a 303 with the `routes.get` URI as location.\n\n#### PUT HTML form template\n\n200 PUT response and GET of a resource shares the same URI and JSON structure, e.g. `/entity/3`\nand `{\"entity\":{\"name\":\"an entity\"}}`.\n\nGET returns an editable detail form of the entitiy with an hidden input field.\n```html\n\u003cform action=\"{*data.entity.routes.put*}\" method=\"post\"\u003e\n  \u003cinput name=\"method\" type=\"hidden\" value=\"put\"\u003e\n  \u003cinput required name=\"subject\" value=\"{{data.entity.name}}\"\u003e\n  \u003cbutton type=\"submit\"\u003eSave\u003c/button\u003e\n\u003c/form\u003e\n```\n\nThe Nginx route changes this POST into a PUT request and finally uses the URI to find\nand render a template of the updated entity.\n\n#### DELETE HTML form template and redirect\n\nA 204 DELETE response is rewritten to a 303 with the `routes.next` URI as location.\n\nDELETE request requires an additional HTML form. A possible convention is the `/delete` path suffix.\n```html\n\u003cform action=\"{*data.routes.confirm*}\" method=\"post\"\u003e\n  \u003cinput name=\"method\" type=\"hidden\" value=\"delete\"\u003e\n  \u003cbutton type=\"submit\"\u003eYes\u003c/button\u003e\n\u003c/form\u003e\n```\n\n[postgres]: http://www.postgresql.org/\n[pg_apt]: http://wiki.postgresql.org/wiki/Apt\n[openresty]: http://openresty.org/\n[lua-resty-template]: https://github.com/bungle/lua-resty-template\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgf%2Fpgx-rest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdgf%2Fpgx-rest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgf%2Fpgx-rest/lists"}