PostgREST-like API for a SQL Server database.
Clone this repo and cd into it:
git clone https://github.com/steve-chavez/mssqlREST
cd mssqlREST
Run SQL Server in Docker:
docker run --name mssqlrest -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=my$ecretPassword' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest
Make a quick test to make sure the instance is started:
docker exec -it mssqlrest /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'my$ecretPassword'
1> select 1
2> go
-----------
1
(1 rows affected)
## Retry after a few seconds if you get the following error:
## Sqlcmd: Error: Microsoft ODBC Driver 17 for SQL Server : Login timeout expired.
## Sqlcmd: Error: Microsoft ODBC Driver 17 for SQL Server : TCP Provider: Error code 0x2749.
## Sqlcmd: Error: Microsoft ODBC Driver 17 for SQL Server : A network-related or instance-specific error has occurred while establishing a connection to SQL Server. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online..
Run the sample database:
docker cp src/test/resources/all.sql mssqlrest:/ && \
docker exec -it mssqlrest /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'my$ecretPassword' -i all.sql
Check the sample database:
docker exec -it mssqlrest /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'my$ecretPassword'
1> use mssqlrest_test
2> go
Changed database context to 'mssqlrest_test'.
1> select * from api.projects
2> go
id name
----------- --------------------------------------------------
1 updated project 1
2 project 2
3 project 3
(3 rows affected)
Download the release and execute it using the sample config:
wget https://github.com/steve-chavez/mssqlREST/releases/download/v1.0.0/mssqlREST.jar
java -jar mssqlREST.jar src/test/resources/config.yml
[Thread-0] INFO org.eclipse.jetty.util.log - Logging initialized @709ms
[Thread-0] INFO spark.webserver.JettySparkServer - == Spark has ignited ...
[Thread-0] INFO spark.webserver.JettySparkServer - >> Listening on 0.0.0.0:9090
[Thread-0] INFO org.eclipse.jetty.server.Server - jetty-9.3.z-SNAPSHOT
[Thread-0] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@6f8cb5b1{HTTP/1.1,[http/1.1]}{0.0.0.0:9090}
[Thread-0] INFO org.eclipse.jetty.server.Server - Started @906ms
Do a request:
curl localhost:9090/projects
[{"name":"updated project 1","id":1},{"name":"project 2","id":2},{"name":"project 3","id":3}]
## The url of the database
url: jdbc:sqlserver://localhost:1433;database=mssqlrest_test;
## The connection user and password
user: authenticator
password: 2wsx!QAZ
## The port of the webserver
port: 9090
## The schema to be exposed
schema: api
## The default role used for every request
defaultRole: anonymous
## the jwt secret
secret: mysecretpass
## the routines(procedure or function) whose return value will be signed by the secret
jwtRoutines :
- login
mssqlREST follows the PostgREST auth guidelines. We have the same tree roles:
CREATE LOGIN authenticator WITH PASSWORD = '2wsx!QAZ'
CREATE USER authenticator FOR LOGIN authenticator
CREATE USER anonymous WITHOUT LOGIN
CREATE USER webuser WITHOUT LOGIN
GRANT IMPERSONATE ON USER::anonymous TO authenticator
GRANT IMPERSONATE ON USER::webuser TO authenticator
For impersonating another role, it uses EXECUTE AS.
This is done in each transaction and at the end REVERT is used.
This is similar to doing a SET ROLE
and DISCARD
in PostgreSQL. There's no equivalent of a SET LOCAL ROLE
in SQL Server.
The routines in the jwtRoutines
config get their result signed(HS256) with the secret
config. We'll use the login
procedure
here as an example:
curl -H "Content-Type: application/json" -d '{"email": "[email protected]", "password": "johnpass"}' l:9090/rpc/login
eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoid2VidXNlciIsImVtYWlsIjoiam9obmRvZUBjb21wYW55LmNvbSJ9.tJf3EOV_bdLgvKBo-NuYbR_JSUPEerrU6bJh9xOoTlQ
This token can be passed in the Authorization
header. mssqlREST obtains the role
claim from the JWT and then it will switch
the role accordingly.
export JWT="eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoid2VidXNlciIsImVtYWlsIjoiam9obmRvZUBjb21wYW55LmNvbSJ9.tJf3EOV_bdLgvKBo-NuYbR_JSUPEerrU6bJh9xOoTlQ"
curl -H "Authorization: Bearer $JWT" l:9090/privileged_projects
[{"name":"priv-project 1","id":1},{"name":"priv-project 2","id":2},{"name":"priv-project 3","id":3}]
Without the header this resource cannot be accessed.
curl l:9090/privileged_projects
{"message":"The resource doesn't exist or permission was denied"}
These formats are available:
- text/csv
curl -H "Accept: text/csv" localhost:9090/projects
id,name
1,updated project 1
2,project 2
3,project 3
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (excel XLSX format)
curl -H "Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" \
localhost:9090/projects > projects.xlsx
- application/json(default, assumed for
*/*
)
The eq
, neq
, gt
, gte
, lt
, lte
and like
operators are supported:
curl localhost:9090/projects?id=eq.1
[{"name":"updated project 1","id":1}]
curl localhost:9090/projects?id=neq.1
[{"name":"project 2","id":2},{"name":"project 3","id":3}]
## the wildcard `%` must be used urlencoded as %25
curl localhost:9090/projects?name=like.%25project%25
[{"name":"updated project 1","id":1},{"name":"project 2","id":2},{"name":"project 3","id":3}]
The select
and order
query params are also supported:
curl "localhost:9090/projects?select=id&order=id.desc"
[{"id":3},{"id":2},{"id":1}]
You can POST with a JSON object to insert a single row.
curl -H "Content-Type: application/json" -d '{"id": "9", "name": "project 9"}' localhost:9090/projects
POSTing with JSON arrays is not supported.
Batch inserts with the CSV and XLSX formats are possible:
curl -H "Content-Type: text/csv" -d $'id,name\n10,project 10\n' localhost:9090/projects
## projects.xlsx with "10, project 10" content
curl -H "Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" \
--data-binary @projects.xlsx localhost:9090/projects
Check the insertions:
curl localhost:9090/projects?id=gte.9
[{"name":"project 9","id":9},{"name":"project 10","id":10}]
PATCH and DELETE can be used as usual:
curl -v -X PATCH -H "Content-Type: application/json" -d '{"name": "recently updated project 1"}' localhost:9090/projects?id=eq.1
curl localhost:9090/projects?id=eq.1
[{"name":"recently updated project 1xxx","id":1}]
curl -X DELETE localhost:9090/projects?id=eq.1
curl localhost:9090/projects?id=eq.1
[]
You can call functions and procedures on the /rpc/<routine>
endpoint with POST. The function parameters need to be a json object.
GET is not supported.
Calling a function that returns a table type:
curl -H "Content-Type: application/json" -d '{"id": 4}' localhost:9090/rpc/get_projects_lt
[{"name":"project 2","id":2},{"name":"project 3","id":3}]
The result of this kind of functions can be in any of the supported formats.
Calling a function that returns a scalar type:
curl -H "Content-Type: application/json" -d '{"a": 1, "b": 2}' localhost:9090/rpc/plus
3
Calling a procedure that returns values in its OUT parameters:
curl -H "Content-Type: application/json" -d '{"x": 2, "y": 4, "z": 6, "factor": 2}' localhost:9090/rpc/mult_xyz_by
{"x":4,"y":8,"z":12}
Procedures that use RETURN to return a table type are not supported. There isn't metadata that allows to infer the RETURN type.
Even SQL server sp_help
doesn't know the return type.
You can get a JSON array of all the available relations at the root.
curl localhost:9090
[
{
"updateable": true,
"selectable": true,
"name": "entities",
"deletable": true,
"insertable": true
},
{
"updateable": true,
"selectable": true,
"name": "items",
"deletable": true,
"insertable": true
},
{
"updateable": true,
"selectable": true,
"name": "projects",
"deletable": true,
"insertable": true
},
{
"updateable": false,
"selectable": true,
"name": "projects_view",
"deletable": false,
"insertable": false
}
]
Also, to get the columns information for an invidual relation you can use /?table=<tablename>
:
curl localhost:9090/?table=projects
[
{
"default": null,
"nullable": false,
"precision": 10,
"name": "id",
"scale": 0,
"type": "int",
"max_length": null
},
{
"default": null,
"nullable": false,
"precision": null,
"name": "name",
"scale": null,
"type": "varchar",
"max_length": 50
}
]
This project had to support an outdated SQL Server 2008 R2. That version didn't have native JSON support, so the JSON response is built in java. SQL Server 2016 and newer versions have JSON AUTO, this could be used for better performance.
There's no schema cache, this is wasteful since for every relation request the information_schema.columns
VIEW is queried. However,
this has the advantage of not needing a schema reload. Adding new relations and routines should work normally with the started server.
For the above reasons, this project is not suitable for a public API. mssqlREST was used for developing an intranet application.
No support for Prefer: return=representation
. Newer SQL Server versions have support for the OUTPUT clause
which could be used to support this.
No support for resource embedding. JSON AUTO could be used for this since it works for JOINs as well: select * from api.projects cross join api.items for json auto
.
For building and running the server:
gradle run
For running the tests:
bash src/test/resources/all.sh && \
docker cp src/test/resources/all.sql mssqlrest:/ && \
docker exec -it mssqlrest /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'my$ecretPassword' -i all.sql &&
gradle test
To generate a jar:
gradle jar
The generated jar will be at ./build/libs/mssqlREST.jar
.