REST APIs from DB Schema Data
Sapid creates instantly-operational REST routes from DB schema data.
-
Creates REST-powered ring routes for different routers including reitit, bidi or Duct-Ataraxy.
-
Supports nested resource structures for
one-to-one
,one-to-many
andmany-to-many
relationships on top ofroot
entities. -
DB schema data can be retrieved from a running DB or specified with a config map selectively.
-
Filtering, sorting and pagination come out of the box.
-
Swagger UI connectable.
- This project is currently in POC state and hasn't been published to Clojars yet.
Here's an example schema showing how Sapid creates endpoints according to four types of relationships: Root
, 1-to-1
, 1-to-N
and N-to-N
. (Please refer to Routes per relationship types for geneic rules.)
;; Read schema data from DB (as data for Integrant)
{:sapid.core/reitit-routes {:db (ig/ref :my-db/connection)}
::app {:routes (ig/ref :sapid.core/reitit-routes)}}
;; Provide schema data (direct function call)
(def routes (sapid.core/make-reitit-routes {:tables [{:name "..."}]}))
;; Read schema data from DB (as data for Integrant)
{:sapid.core/bidi-routes {:db (ig/ref :my-db/connection)}
::app {:routes (ig/ref :sapid.core/reitit-routes)}}
;; Provide schema data (direct function call)
(def routes (sapid.core/make-bidi-routes {:tables [{:name "..."}]}))
;; at root/module level of duct config edn
:sapid.core/duct-routes {}
When reading schema data from DB connection, Sapid leverages naming patterns of tables/columns to identify relationships:
Table names can be specified in the config map for other naming patterns. (In this case, Sapid will not attempt retrieving schema data from DB.)
-
Root
orN-to-N
relationship?A table name without
_
would be classified asRoot
, and a table name pattern ofresourcea_resourceb
(likemembers_groups
) is assumed forN-to-N
tables. -
1-to-1
/1-to-N
relationship?If a table is not
N-to-N
and contains a column ending with_id
,1-to-1
/1-to-N
relationship is identified per column.
Though configurable parameters vary by router types, Sapid doesn't require many config values in general. Some key concepts & list of parameters are as below:
Key | Description | Default Value |
---|---|---|
:db |
Database connection object. | |
:table-name-plural |
true if tables uses plural naming like users instead of user . |
true |
:resource-path-plural |
true if plural is desired for URL paths like /users instead of /user . |
true |
:tables |
DB schema including list of table definitions. Plz check Schema Data for details. | Created from :db |
Schema data is used to specify custom table schema to construct REST APIs without querying a DB. It is specified with a list of tables under :tables
key in the config map.
{:tables [
{:relation-types [:root :one-n]
:name "users"
:columns [{:name "id"
:type "text"
:notnull 0
:dflt_value nil}
{:name "image_id"
:type "int"
:notnull 1
:dflt_value 1}
;; ... more columns
]
:belongs-to ["image"]
:pre-save-signal #ig/ref :my-project/user-pre-save-fn
:post-save-signal #ig/ref :my-project/user-post-save-fn}
;; ... more tables
]}
Key | Description |
---|---|
:name |
Table name. |
:columns |
List of columns. A column can contain :name , :type , :notnull and :dflt_value parameters. |
:relation-types |
List of table relation types. :root , :one-n and :n-n are supported. |
:belongs-to |
List of columns related to id of other tables. (:table-name-plural will format them accordingly.) |
:pre-save-signal |
A function to be triggered at handler before accessing DB. (It will be triggered with request as a parameter.) |
:post-save-signal |
A function to be triggered at handler after accessing DB. (It will be triggered with result data as a parameter.) |
- Parameters specific to Duct Ataraxy
Key | Description | Default Value |
---|---|---|
:project-ns |
Project namespace. It'll be used for route keys. | Loaded from :duct.core |
:db-config-key |
Integrant key for a database connection. | :duct.database/sql |
:db |
Database connection object. If provided Sapid won't init the :db-config-key. | Created from :db-config-key |
:db-ref |
Integrant reference to a database connection for REST handler configs. | Created from :db-config-key |
:db-keys |
Keys to get a connection from a database map. | [:spec] |
Format of ?column=[operator]:[value]
is used in a query string for filtering.
?id=lt:100&id=ne:1
(where id
is less than 100
AND
id
is not equal to 1
)
- Supported operators are
eq
,ne
,lt
,le
/lte
,gt
, andge
/gte
.- Operators default to
eq
when omitted.- Multiple queries are applied with
AND
operator.
Format of ?order-by=[column]:[asc or desc]
is used in a query string for sorting.
?order-by=id:desc
(sort by id
column in descending order)
- Direction defaults to
desc
when omitted.
Formats of limit=[count]
and offset=[count]
are used in a query string for pagination.
?limit=20&id=gt:20
(20 items after/greater than id
:20
.)
limit
andoffset
can be used independently.- Using
offset
can return different results when new entries are created while items are sorted by newest first. So usinglimit
withid
filter orcreated_at
filter is often considered more consistent.
Generic rules of route creation per relatioship types are as below:
Root
HTTP methods | Routes |
---|---|
GET , POST |
/resource |
GET , DELETE , PUT and PATCH |
/resource/{id} |
1-to-1
/1-to-N
HTTP methods | Routes |
---|---|
GET and POST |
/parent-resource/{parent-id}/child-resource |
GET , DELETE , PUT and PATCH |
/parent-resource/{parent-id}/child-resource/{child-id} |
N-to-N
HTTP methods | Routes |
---|---|
GET |
/resource-a/{id-of-a}/resource-b |
GET |
/resource-b/{id-of-b}/resource-a |
POST |
/resource-a/{id-of-a}/resource-b/{id-of-b}/add |
POST |
/resource-b/{id-of-b}/resource-a/{id-of-a}/add |
POST |
/resource-a/{id-of-a}/resource-b/{id-of-b}/delete |
POST |
/resource-b/{id-of-b}/resource-a/{id-of-a}/delete |
To begin developing, start with a REPL.
lein repl
Then load the development environment.
user=> (dev)
:loaded
Run go
to prep and initiate the system.
dev=> (go)
:duct.server.http.jetty/starting-server {:port 3000}
:initiated
By default this creates a web server at http://localhost:3000.
When you make changes to your source files, use reset
to reload any
modified files and reset the server.
dev=> (reset)
:reloading (...)
:resumed
Testing is fastest through the REPL, as you avoid environment startup time.
dev=> (test)
...
But you can also run tests through Leiningen.
lein test
Copyright © 2021 Yohei Kusakabe