Merge pull request #56 from thevahidal/api-examples-doc-rows

Api examples doc rows
This commit is contained in:
Vahid Al
2022-11-03 19:07:11 +03:30
committed by GitHub
8 changed files with 444 additions and 100 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "soul-cli",
"version": "0.0.6",
"version": "0.0.7",
"description": "A SQLite RESTful server",
"main": "src/server.js",
"bin": {

View File

@@ -34,8 +34,12 @@ const listTableRows = async (req, res) => {
description: 'Extend rows. e.g. ?_extend=user_id will return user data for each row.',
in: 'query',
}
#swagger.parameters['_filters'] = {
description: 'Filter rows. e.g. ?_filters=name:John,age:20 will return rows where name is John and age is 20.',
in: 'query',
}
*/
const { name } = req.params;
const { name: tableName } = req.params;
const {
_page = 1,
_limit = 10,
@@ -43,41 +47,48 @@ const listTableRows = async (req, res) => {
_ordering,
_schema,
_extend,
...filters
_filters = '',
} = req.query;
const page = parseInt(_page);
const limit = parseInt(_limit);
// if filters are provided, filter rows by them
// filters consists of fields to filter by
// filtering must be case insensitive
// e.g. ?name=John&age=20
// if _filters are provided, filter rows by them
// _filters consists of fields to filter by
// filtering is case insensitive
// e.g. ?_filters=name:John,age:20
// will filter by name like '%John%' and age like '%20%'
const filters = _filters.split(',').map((filter) => {
const [field, value] = filter.split(':');
return { field, value };
});
let whereString = '';
if (Object.keys(filters).length > 0) {
if (_filters !== '') {
whereString += ' WHERE ';
whereString += Object.keys(filters)
.map((key) => `${key} LIKE '%${filters[key]}%'`)
whereString += filters
.map((filter) => `${tableName}.${filter.field} LIKE '%${filter.value}%'`)
.join(' AND ');
}
// if _search is provided, search rows by it
// e.g. ?_search=John will search for John in all fields of the table
// searching must be case insensitive
// searching is case insensitive
if (_search) {
if (whereString) {
if (whereString !== '') {
whereString += ' AND ';
} else {
whereString += ' WHERE ';
}
try {
// get all fields of the table
const fields = db.prepare(`PRAGMA table_info(${name})`).all();
const fields = db.prepare(`PRAGMA table_info(${tableName})`).all();
whereString += '(';
whereString += fields
.map((field) => `${field.name} LIKE '%${_search}%'`)
.map((field) => `${tableName}.${field.name} LIKE '%${_search}%'`)
.join(' OR ');
whereString += ')';
} catch (error) {
return res.status(400).json({
message: error.message,
@@ -104,10 +115,10 @@ const listTableRows = async (req, res) => {
if (_schema) {
const schemaFields = _schema.split(',');
schemaFields.forEach((field) => {
schemaString += `${name}.${field},`;
schemaString += `${tableName}.${field},`;
});
} else {
schemaString = `${name}.*`;
schemaString = `${tableName}.*`;
}
// remove trailing comma
@@ -143,31 +154,36 @@ const listTableRows = async (req, res) => {
let extendString = '';
if (_extend) {
const extendFields = _extend.split(',');
extendFields.forEach((field) => {
extendFields.forEach((extendedField) => {
try {
const foreignKey = db
.prepare(`PRAGMA foreign_key_list(${name})`)
.prepare(`PRAGMA foreign_key_list(${tableName})`)
.all()
.find((fk) => fk.from === field);
.find((fk) => fk.from === extendedField);
if (!foreignKey) {
throw new Error('Foreign key not found');
}
const { table } = foreignKey;
const { table: joinedTableName } = foreignKey;
const fields = db.prepare(`PRAGMA table_info(${table})`).all();
const joinedTableFields = db
.prepare(`PRAGMA table_info(${joinedTableName})`)
.all();
extendString += ` LEFT JOIN ${table} as ${table} ON ${table}.${foreignKey.to} = ${name}.${field}`;
extendString += ` LEFT JOIN ${joinedTableName} ON ${joinedTableName}.${foreignKey.to} = ${tableName}.${extendedField}`;
// joined fields will be returned in a new object called {field}_data e.g. author_id_data
const extendFieldsString =
'json_object( ' +
fields
.map((field) => `'${field.name}', ${table}.${field.name}`)
joinedTableFields
.map(
(joinedTableField) =>
`'${joinedTableField.name}', ${joinedTableName}.${joinedTableField.name}`
)
.join(', ') +
' ) as ' +
field +
extendedField +
'_data';
if (schemaString) {
@@ -185,21 +201,34 @@ const listTableRows = async (req, res) => {
}
// get paginated rows
const query = `SELECT ${schemaString} FROM ${name} ${extendString} ${whereString} ${orderString} LIMIT ${limit} OFFSET ${
const query = `SELECT ${schemaString} FROM ${tableName} ${extendString} ${whereString} ${orderString} LIMIT ${limit} OFFSET ${
limit * (page - 1)
}`;
try {
const data = db.prepare(query).all();
let data = db.prepare(query).all();
// parse json extended files
if (_extend) {
const extendFields = _extend.split(',');
data = data.map((row) => {
Object.keys(row).forEach((key) => {
if (extendFields.includes(key.replace('_data', ''))) {
row[key] = JSON.parse(row[key]);
}
});
return row;
});
}
// get total number of rows
const total = db
.prepare(`SELECT COUNT(*) as total FROM ${name} ${whereString}`)
.prepare(`SELECT COUNT(*) as total FROM ${tableName} ${whereString}`)
.get().total;
const next =
data.length === limit ? `/tables/${name}?page=${page + 1}` : null;
const previous = page > 1 ? `/tables/${name}?page=${page - 1}` : null;
data.length === limit ? `/tables/${tableName}?page=${page + 1}` : null;
const previous = page > 1 ? `/tables/${tableName}?page=${page - 1}` : null;
res.json({
data,
@@ -236,7 +265,7 @@ const insertRowInTable = async (req, res) => {
}
*/
const { name } = req.params;
const { name: tableName } = req.params;
const { fields } = req.body;
const fieldsString = Object.keys(fields).join(', ');
@@ -250,7 +279,13 @@ const insertRowInTable = async (req, res) => {
})
.join(', ');
const query = `INSERT INTO ${name} (${fieldsString}) VALUES (${valuesString})`;
let values = `(${fieldsString}) VALUES (${valuesString})`;
if (valuesString === '') {
values = 'DEFAULT VALUES';
}
const query = `INSERT INTO ${tableName} ${values}`;
try {
const data = db.prepare(query).run();
@@ -315,7 +350,7 @@ const getRowInTableByPK = async (req, res) => {
type: 'string'
}
#swagger.parameters['_field'] = {
#swagger.parameters['_lookup_field'] = {
in: 'query',
description: 'If you want to get field by any other field than primary key, use this parameter',
required: false,
@@ -323,17 +358,24 @@ const getRowInTableByPK = async (req, res) => {
}
*/
const { name, pk } = req.params;
const { _field, _schema, _extend } = req.query;
const { name: tableName, pk } = req.params;
const { _lookup_field, _schema, _extend } = req.query;
let searchField = _field;
let lookupField = _lookup_field;
if (!_field) {
if (!_lookup_field) {
// find the primary key of the table
searchField = db
.prepare(`PRAGMA table_info(${name})`)
.all()
.find((field) => field.pk === 1).name;
try {
lookupField = db
.prepare(`PRAGMA table_info(${tableName})`)
.all()
.find((field) => field.pk === 1).name;
} catch (error) {
return res.status(400).json({
message: error.message,
error: error,
});
}
}
// if _schema is provided, return only those fields
@@ -344,10 +386,10 @@ const getRowInTableByPK = async (req, res) => {
if (_schema) {
const schemaFields = _schema.split(',');
schemaFields.forEach((field) => {
schemaString += `${name}.${field},`;
schemaString += `${tableName}.${field},`;
});
} else {
schemaString = `${name}.*`;
schemaString = `${tableName}.*`;
}
// remove trailing comma
@@ -383,31 +425,36 @@ const getRowInTableByPK = async (req, res) => {
let extendString = '';
if (_extend) {
const extendFields = _extend.split(',');
extendFields.forEach((field) => {
extendFields.forEach((extendedField) => {
try {
const foreignKey = db
.prepare(`PRAGMA foreign_key_list(${name})`)
.prepare(`PRAGMA foreign_key_list(${tableName})`)
.all()
.find((fk) => fk.from === field);
.find((fk) => fk.from === extendedField);
if (!foreignKey) {
throw new Error('Foreign key not found');
}
const { table } = foreignKey;
const { table: joinedTableName } = foreignKey;
const fields = db.prepare(`PRAGMA table_info(${table})`).all();
const joinedTableFields = db
.prepare(`PRAGMA table_info(${joinedTableName})`)
.all();
extendString += ` LEFT JOIN ${table} as ${table} ON ${table}.${foreignKey.to} = ${name}.${field}`;
extendString += ` LEFT JOIN ${joinedTableName} ON ${joinedTableName}.${foreignKey.to} = ${tableName}.${extendedField}`;
// joined fields will be returned in a new object called {field}_data e.g. author_id_data
const extendFieldsString =
'json_object( ' +
fields
.map((field) => `'${field.name}', ${table}.${field.name}`)
joinedTableFields
.map(
(joinedTableField) =>
`'${joinedTableField.name}', ${joinedTableName}.${joinedTableField.name}`
)
.join(', ') +
' ) as ' +
field +
extendedField +
'_data';
if (schemaString) {
@@ -424,13 +471,23 @@ const getRowInTableByPK = async (req, res) => {
});
}
const query = `SELECT ${schemaString} FROM ${name} ${extendString} WHERE ${name}.${searchField} = ${pk}`;
const query = `SELECT ${schemaString} FROM ${tableName} ${extendString} WHERE ${tableName}.${lookupField} = '${pk}'`;
try {
const data = db.prepare(query).get();
let data = db.prepare(query).get();
// parse json extended files
if (_extend) {
const extendFields = _extend.split(',');
Object.keys(data)
.filter((key) => extendFields.includes(key.replace('_data', '')))
.forEach((key) => {
data[key] = JSON.parse(data[key]);
});
}
if (!data) {
res.status(404).json({
return res.status(404).json({
message: 'Row not found',
error: 'not_found',
});
@@ -440,7 +497,7 @@ const getRowInTableByPK = async (req, res) => {
});
}
} catch (error) {
res.status(400).json({
return res.status(400).json({
message: error.message,
error: error,
});
@@ -472,7 +529,7 @@ const updateRowInTableByPK = async (req, res) => {
schema: { $ref: "#/definitions/UpdateRowRequestBody" }
}
#swagger.parameters['_field'] = {
#swagger.parameters['_lookup_field'] = {
in: 'query',
description: 'If you want to update row by any other field than primary key, use this parameter',
required: false,
@@ -480,18 +537,25 @@ const updateRowInTableByPK = async (req, res) => {
}
*/
const { name, pk } = req.params;
const { name: tableName, pk } = req.params;
const { fields } = req.body;
const { _field } = req.query;
const { _lookup_field } = req.query;
let searchField = _field;
let lookupField = _lookup_field;
if (!_field) {
if (!_lookup_field) {
// find the primary key of the table
searchField = db
.prepare(`PRAGMA table_info(${name})`)
.all()
.find((field) => field.pk === 1).name;
try {
lookupField = db
.prepare(`PRAGMA table_info(${tableName})`)
.all()
.find((field) => field.pk === 1).name;
} catch (error) {
return res.status(400).json({
message: error.message,
error: error,
});
}
}
// wrap text values in quotes
@@ -505,7 +569,14 @@ const updateRowInTableByPK = async (req, res) => {
})
.join(', ');
const query = `UPDATE ${name} SET ${fieldsString} WHERE ${searchField} = '${pk}'`;
if (fieldsString === '') {
return res.status(400).json({
message: 'No fields provided',
error: 'no_fields_provided',
});
}
const query = `UPDATE ${tableName} SET ${fieldsString} WHERE ${lookupField} = '${pk}'`;
try {
db.prepare(query).run();
@@ -537,7 +608,7 @@ const deleteRowInTableByPK = async (req, res) => {
description: 'Primary key',
required: true,
}
#swagger.parameters['_field'] = {
#swagger.parameters['_lookup_field'] = {
in: 'query',
description: 'If you want to delete row by any other field than primary key, use this parameter',
required: false,
@@ -545,30 +616,45 @@ const deleteRowInTableByPK = async (req, res) => {
}
*/
const { name, pk } = req.params;
const { _field } = req.query;
const { name: tableName, pk } = req.params;
const { _lookup_field } = req.query;
let searchField = _field;
let lookupField = _lookup_field;
if (!_field) {
if (!_lookup_field) {
// find the primary key of the table
searchField = db
.prepare(`PRAGMA table_info(${name})`)
.all()
.find((field) => field.pk === 1).name;
try {
lookupField = db
.prepare(`PRAGMA table_info(${tableName})`)
.all()
.find((field) => field.pk === 1).name;
} catch (error) {
return res.status(400).json({
message: error.message,
error: error,
});
}
}
const query = `DELETE FROM ${name} WHERE ${searchField} = '${pk}'`;
const data = db.prepare(query).run();
const query = `DELETE FROM ${tableName} WHERE ${lookupField} = '${pk}'`;
if (data.changes === 0) {
res.status(404).json({
error: 'not_found',
});
} else {
res.json({
message: 'Row deleted',
data,
try {
const data = db.prepare(query).run();
if (data.changes === 0) {
res.status(404).json({
error: 'not_found',
});
} else {
res.json({
message: 'Row deleted',
data,
});
}
} catch (error) {
res.status(400).json({
message: error.message,
error: error,
});
}
};

View File

@@ -206,8 +206,8 @@ const getTableSchema = async (req, res) => {
}
*/
const { name } = req.params;
const query = `PRAGMA table_info(${name})`;
const { name: tableName } = req.params;
const query = `PRAGMA table_info(${tableName})`;
try {
const schema = db.prepare(query).all();
@@ -236,8 +236,8 @@ const deleteTable = async (req, res) => {
}
*/
const { name } = req.params;
const query = `DROP TABLE ${name}`;
const { name: tableName } = req.params;
const query = `DROP TABLE ${tableName}`;
try {
db.prepare(query).run();

View File

@@ -1,6 +1,5 @@
const Joi = require('joi');
// allow other fields for filtering
const listTableRows = Joi.object({
name: Joi.string().min(3).max(30).required(),
_page: Joi.number().integer().min(1).default(1),
@@ -9,6 +8,7 @@ const listTableRows = Joi.object({
_ordering: Joi.string(),
_schema: Joi.string(),
_extend: Joi.string(),
_filters: Joi.string(),
}).unknown(true);
const insertRowInTable = Joi.object({
@@ -19,7 +19,7 @@ const insertRowInTable = Joi.object({
const getRowInTableByPK = Joi.object({
name: Joi.string().min(3).max(30).required(),
pk: Joi.string().required(),
_field: Joi.string().min(3).max(30),
_lookup_field: Joi.string().min(3).max(30),
_schema: Joi.string(),
_extend: Joi.string(),
});
@@ -28,13 +28,13 @@ const updateRowInTableByPK = Joi.object({
name: Joi.string().min(3).max(30).required(),
pk: Joi.string().required(),
fields: Joi.object().required(),
_field: Joi.string().min(3).max(30),
_lookup_field: Joi.string().min(3).max(30),
});
const deleteRowInTableByPK = Joi.object({
name: Joi.string().min(3).max(30).required(),
pk: Joi.string().required(),
_field: Joi.string().min(3).max(30),
_lookup_field: Joi.string().min(3).max(30),
});
module.exports = {

View File

@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "0.0.6",
"version": "0.0.7",
"title": "Soul API",
"description": "API Documentation for <b>Soul</b>, a simple SQLite RESTful server. "
},
@@ -237,12 +237,13 @@
"type": "string"
},
{
"name": "_search",
"name": "_filters",
"description": "Filter rows. e.g. ?_filters=name:John,age:20 will return rows where name is John and age is 20.",
"in": "query",
"type": "string"
},
{
"name": "filters",
"name": "_search",
"in": "query",
"type": "string"
}
@@ -332,7 +333,7 @@
"type": "string"
},
{
"name": "_field",
"name": "_lookup_field",
"in": "query",
"description": "If you want to get field by any other field than primary key, use this parameter",
"required": false,
@@ -381,7 +382,7 @@
}
},
{
"name": "_field",
"name": "_lookup_field",
"in": "query",
"description": "If you want to update row by any other field than primary key, use this parameter",
"required": false,
@@ -439,7 +440,7 @@
"description": "Primary key"
},
{
"name": "_field",
"name": "_lookup_field",
"in": "query",
"description": "If you want to delete row by any other field than primary key, use this parameter",
"required": false,
@@ -450,6 +451,9 @@
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Not Found"
}

View File

@@ -4,7 +4,7 @@ Soul is consist of 3 main namespaces: `/tables`, `/rows` and `/`. In this docume
## Setup Environment
To follow the below examples run Soul with `sample.db` provided in `examples/` directory:
To follow the below examples we need to download a sample database and also install Soul CLI.
### Download Sample Database

41
docs/api/root-examples.md Normal file
View File

@@ -0,0 +1,41 @@
## Root
### 1. Transaction
To start a transaction call `/transaction` endpoint with `POST` method.
```bash
curl --request POST \
--url http://localhost:8000/api/transaction \
--header 'Content-Type: application/json' \
--data '{
"transaction": [
{
"statement": "INSERT INTO Artist (ArtistId, Name) VALUES (:id, :name)",
"values": { "id": 100000, "name": "Glen Hansard" }
},
{
"query": "SELECT * FROM Artist ORDER BY ArtistId DESC LIMIT 1"
}
]
}'
```
Response
```json
{
"data": [
{
"changes": 1,
"lastInsertRowid": 100000
},
[
{
"ArtistId": 100000,
"Name": "Glen Hansard"
}
]
]
}
```

213
docs/api/rows-examples.md Normal file
View File

@@ -0,0 +1,213 @@
## Rows
### 1. List Rows of a Table
To list all (or some of) rows we simply call `/tables/<table-name>/rows/` endpoint with `GET` method.
```bash
curl 'localhost:8000/api/tables/Album/rows/'
```
Response
```json
{
"data": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You",
"ArtistId": 1
},
{ "AlbumId": 2, "Title": "Balls to the Wall", "ArtistId": 2 }
// ...
],
"total": 347,
"next": "/tables/Album?page=2",
"previous": null
}
```
#### Query Params
- `_page` e.g. `?_page=2`, to get the second page of results.
- `_limit` e.g. `?_limit=20`, to get 20 results per page.
- `_search` e.g. `?_search=rock`, to search between rows.
- `_ordering` e.g. `?_ordering=-Title`, to order rows by title descending, or without `-` to sort ascending, e.g. `?_ordering=Title`
- `_schema` e.g. `?_schema=Title,ArtistId`, to get only the Title and ArtistId columns.
- `_extend` e.g. `?_extend=ArtistId`, to get the Artist object related to the Album.
- `_filters` e.g. `?_filters=ArtistId:1,Title:Rock`, to get only the rows where the ArtistId is 1 and the Title is Rock.
Example with query params
```bash
curl 'localhost:8000/api/tables/Album/rows?_page=1&_limit=20&_search=rock&_ordering=-Title&_schema=Title,ArtistId&_extend=ArtistId&_filters=ArtistId:90'
```
Response
```json
{
"data": [
{
"Title": "Rock In Rio [CD2]",
"ArtistId": 90,
"ArtistId_data": { "ArtistId": 90, "Name": "Iron Maiden" }
},
{
"Title": "Rock In Rio [CD1]",
"ArtistId": 90,
"ArtistId_data": { "ArtistId": 90, "Name": "Iron Maiden" }
}
],
"total": 2,
"next": null,
"previous": null
}
```
### 2. Insert a New Row
To insert a new row to a `table` call `/tables/<table-name>/rows/` endpoint with `POST` method.
```bash
curl --request POST \
--url http://localhost:8000/api/tables/Employee/rows \
--header 'Content-Type: application/json' \
--data '{
"fields": {
"FirstName": "Damien",
"LastName": "Rice"
}
}'
```
Response
```json
{
"message": "Row inserted",
"data": {
"changes": 1,
"lastInsertRowid": 9
}
}
```
#### Body Params
- `fields` e.g.
```json
"fields": {
// fields values for the new row
}
```
### 3. Get a Row
To get a row call `/tables/<table-name>/rows/<lookup-value>/` endpoint with `GET` method.
```bash
curl http://localhost:8000/api/tables/Album/rows/1/
```
Response
```json
{
"data": {
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You",
"ArtistId": 1
}
}
```
#### Query Params
- `_lookup_field` e.g. `?_lookup_field=ArtistId`, to get the row by the ArtistId field. If not provided, the default lookup field is the primary key of the table.
- `_schema` e.g. `?_schema=Title,ArtistId`, to get only the Title and ArtistId columns.
- `_extend` e.g. `?_extend=ArtistId`, to get the Artist object related to the Album.
Example with query params
```bash
curl 'http://localhost:8000/api/tables/Album/rows/Facelift?_lookup_field=Title&_extend=ArtistId&_schema=Title'
```
Response
```json
{
"data": {
"Title": "Facelift",
"ArtistId_data": {
"ArtistId": 5,
"Name": "Alice In Chains"
}
}
}
```
### 4. Update a Row
To update a row call `/tables/<table-name>/rows/<lookup-value>/` endpoint with `PUT` method.
```bash
curl --request PUT \
--url http://localhost:8000/api/tables/Album/rows/7 \
--header 'Content-Type: application/json' \
--data '{
"fields": {
"Title": "FaceElevate"
}
}'
```
Response
```json
{
"message": "Row updated"
}
```
#### Query Params
- `_lookup_field` e.g. `?_lookup_field=ArtistId`, to update the row by the ArtistId field. If not provided, the default lookup field is the primary key of the table.
#### Body Params
- `fields` e.g.
```json
"fields": {
// fields values to update
}
```
### 5. Delete a Row
To delete a row call `/tables/<table-name>/rows/<lookup-value>/` endpoint with `DELETE` method.
```bash
curl --request DELETE \
--url http://localhost:8000/api/tables/PlaylistTrack/rows/1
```
Response
```json
{
"message": "Row deleted",
"data": {
"changes": 3290,
"lastInsertRowid": 0
}
}
```
#### Query Params
- `_lookup_field` e.g. `?_lookup_field=ArtistId`, to delete the row by the ArtistId field. If not provided, the default lookup field is the primary key of the table.