aboutsummaryrefslogtreecommitdiff
path: root/query
diff options
context:
space:
mode:
Diffstat (limited to 'query')
-rw-r--r--query/README.md51
-rw-r--r--query/batch.md63
-rw-r--r--query/count.md48
-rw-r--r--query/delete.md46
-rw-r--r--query/filters.md151
-rw-r--r--query/insert.md50
-rw-r--r--query/select.md67
-rw-r--r--query/update.md54
8 files changed, 530 insertions, 0 deletions
diff --git a/query/README.md b/query/README.md
new file mode 100644
index 0000000..54e99af
--- /dev/null
+++ b/query/README.md
@@ -0,0 +1,51 @@
+# Query
+
+[back to index](../README.md)
+
+The `/query` endpoint is where all database operations happen. One endpoint, one JSON DERULO format, multiple actions. Pretty much the heart of jsonderulo <3 and whats the minimum requirement for JSON DERULO v1 compliance (in addition to authentication).
+
+## Pages
+
+### Actions
+- [Select](select.md) read rows from tables
+- [Insert](insert.md) add new rows
+- [Update](update.md) modify existing rows
+- [Delete](delete.md) remove rows
+- [Count](count.md) count matching rows
+
+### Features
+- [Filters](filters.md) where clauses, structured filters, joins, ordering
+- [Batch](batch.md) run multiple queries of the same type in one request
+
+## How it works
+
+`POST /query` with a JSONDERULO body. The `action` field tells the server what you want to do. The `table` field tells it which table.
+
+```json
+{
+ "action": "select",
+ "table": "items"
+}
+```
+
+Thats the simplest possible query. Selects all columns from the items table with the default limit applied.
+
+Every query should run inside a transaction. The server should set `@current_user_id` and `@request_id` as SQL session variables before running your query so triggers and stuff can reference them if needed in your schema.
+
+## Auth
+
+Bearer token required. The server should ALWAYS check your permissions for the target table (and any joined tables) before doing anything. See [permissions](../permissions/README.md) for how that works.
+
+## Common Response Fields
+
+Every response has `success` (boolean). Beyond that:
+
+| Action | Extra fields |
+|--------|----------------------------------------------|
+| select | `data` (array of rows), optionally `warning` |
+| insert | `data` (last insert id), `rows_affected` |
+| update | `rows_affected`, if applicable `warning` |
+| delete | `rows_affected`, if applicable `warning` |
+| count | `data` (the count as a number) |
+
+The `warning` field should appear when json derulo striped for you, example : your limit being capped to a maximum or blocked columns being stripped away by json derulo stripping. its not an error just a hey bbgurl please comply to what you're allowed to do and to json derulos orders!
diff --git a/query/batch.md b/query/batch.md
new file mode 100644
index 0000000..471adf5
--- /dev/null
+++ b/query/batch.md
@@ -0,0 +1,63 @@
+# Batch Queries
+
+[back to query](README.md) /// [home](../README.md)
+
+Run multiple queries of the same type in one request. All should share the same `action` and `table`.
+
+## Request
+
+```json
+{
+ "action": "insert",
+ "table": "items",
+ "queries": [
+ { "data": { "name": "Widget A", "price": 10 } },
+ { "data": { "name": "Widget B", "price": 20 } },
+ { "data": { "name": "Widget C", "price": 30 } }
+ ],
+ "rollback_on_error": true
+}
+```
+
+## Fields
+
+| Field | Type | Required | Notes |
+|---------------------|---------|----------|-------------------------------------------------------------|
+| `action` | string | yes | shared across all sub queries |
+| `table` | string | yes | shared across all sub queries |
+| `queries` | array | yes | array of individual query bodies (without action and table) |
+| `rollback_on_error` | boolean | no | default false. if true, any failure rolls back everything |
+
+Each item in `queries` can have the same fields as a normal single query (columns, data, where, filter, limit, offset, order_by) just without repeating action and table.
+
+## Permission
+
+Batch operations should require the `allow_batch_operations` permission for your power level (on `jde_groups`). Without it you get a 403.
+
+Someday better granularity of this permission might be introduced but for now this should suffice.
+
+## How it should work
+
+- everything runs in a single transaction
+- for INSERT batches the server should ideally be smart about it and combine them into one multi row INSERT statement instead of doing N individual inserts (way faster)
+- for SELECT and COUNT each sub query runs individually within the transaction
+- if `rollback_on_error` is true and any sub query fails the entire batch should be rolled back and nothing committed
+- if `rollback_on_error` is false (default) failures are reported but successful queries still commit
+
+## Response
+
+Success looks the same as the individual action responses but with a `results` array for partial success/failure scenarios.
+
+When `rollback_on_error` is true and something fails:
+
+```json
+{
+ "success": false,
+ "error": "Batch operation failed, all changes rolled back [request_id: abc123]",
+ "results": [
+ { "success": true, "rows_affected": 1 },
+ { "success": false, "error": "duplicate key" },
+ { "success": true, "rows_affected": 1 }
+ ]
+}
+```
diff --git a/query/count.md b/query/count.md
new file mode 100644
index 0000000..bb4e14d
--- /dev/null
+++ b/query/count.md
@@ -0,0 +1,48 @@
+# Count
+
+[back to query](README.md) /// [home](../README.md)
+
+Count rows matching a filter. Like select but just gives you the number in data field.
+
+## Request
+
+```json
+{
+ "action": "count",
+ "table": "items",
+ "filter": {
+ "column": "status",
+ "op": "=",
+ "value": "active"
+ }
+}
+```
+
+## Fields
+
+| Field | Type | Required | Notes |
+|----------|--------|----------|-----------------------------|
+| `action` | string | yes | must be `"count"` |
+| `table` | string | yes | target table |
+| `where` | object | no | simple key value filter |
+| `filter` | object | no | structured filter |
+| `joins` | array | no | join other tables if needed |
+
+No limit or offset for count, it always counts everything that matches.
+
+## Response
+
+```json
+{
+ "success": true,
+ "data": 42
+}
+```
+
+`data` is just the count as a number.
+
+## Behaviors
+
+- should support joins so you can count rows with conditions on related tables
+- ownership scoping should apply: if you have `rwo` or `rwg` the count should only include rows you have access to
+- you need at least read permission on the table
diff --git a/query/delete.md b/query/delete.md
new file mode 100644
index 0000000..db55292
--- /dev/null
+++ b/query/delete.md
@@ -0,0 +1,46 @@
+# Delete
+
+[back to query](README.md) /// [home](../README.md)
+
+Remove rows from a table.
+
+## Request
+
+```json
+{
+ "action": "delete",
+ "table": "items",
+ "filter": {
+ "column": "id",
+ "op": "=",
+ "value": 42
+ }
+}
+```
+
+## Fields
+
+| Field | Type | Required | Notes |
+|----------|--------|----------|-------------------------|
+| `action` | string | yes | must be `"delete"` |
+| `table` | string | yes | target table |
+| `where` | object | yes* | simple key value filter |
+| `filter` | object | yes* | structured filter |
+
+*at least one of `where` or `filter` is required. no unfiltered deletes lol.
+
+## Response
+
+```json
+{
+ "success": true,
+ "rows_affected": 1
+}
+```
+
+## Behaviors
+
+- WHERE clause is **mandatory**. same as update, no blanket deletes allowed. jsonderulo doesnt want you nuking entire tables on accident
+- ownership scoping should be applied based on your permission level
+- limit should be applied and capped to your max
+- you need write permission on the table (`rw`, `rwa`, `rwo`, `rwg`)
diff --git a/query/filters.md b/query/filters.md
new file mode 100644
index 0000000..13a929b
--- /dev/null
+++ b/query/filters.md
@@ -0,0 +1,151 @@
+# Filters
+
+[back to query](README.md) /// [home](../README.md)
+
+How to filter, join and sort your query results.
+
+## Simple Where
+
+The `where` field is the lazy cunt way. Just key value pairs, all treated as `= value` with AND between them.
+
+```json
+{
+ "action": "select",
+ "table": "items",
+ "where": {
+ "status": "active",
+ "category_id": 3
+ }
+}
+```
+
+This becomes `WHERE status = 'active' AND category_id = 3`.
+
+## Structured Filter
+
+The `filter` field gives you full crazy schizo control. Supports operators, nesting, AND/OR/NOT.
+
+### Single condition
+
+```json
+{ "column": "price", "op": ">", "value": 10 }
+```
+
+### AND (all must match)
+
+```json
+{
+ "and": [
+ { "column": "status", "op": "=", "value": "active" },
+ { "column": "price", "op": ">=", "value": 5 }
+ ]
+}
+```
+
+### OR (any can match)
+
+```json
+{
+ "or": [
+ { "column": "status", "op": "=", "value": "draft" },
+ { "column": "status", "op": "=", "value": "active" }
+ ]
+}
+```
+
+### NOT
+
+```json
+{
+ "not": { "column": "status", "op": "=", "value": "deleted" }
+}
+```
+
+### Nesting
+
+You can nest these however you want:
+
+```json
+{
+ "and": [
+ { "column": "price", "op": ">", "value": 0 },
+ {
+ "or": [
+ { "column": "status", "op": "=", "value": "active" },
+ { "column": "is_featured", "op": "=", "value": true }
+ ]
+ }
+ ]
+}
+```
+
+### Supported Operators
+
+| Operator | What it does |
+|---------------|-------------------------------------------------|
+| `=` | equals |
+| `!=` | not equals |
+| `>` | greater than |
+| `>=` | greater than or equal |
+| `<` | less than |
+| `<=` | less than or equal |
+| `like` | SQL LIKE pattern matching |
+| `not_like` | inverse of LIKE |
+| `in` | value is in a list (pass an array as value) |
+| `not_in` | value is not in a list |
+| `is_null` | column is NULL (no value needed) |
+| `is_not_null` | column is not NULL (no value needed) |
+| `between` | between two values (pass `[min, max]` as value) |
+
+### Maximum Conditions
+
+Theres a per power level limit on how many conditions you can have in one query (`max_where_conditions` on the group). If you exceed it the server should give you a 400. this is to prevent people from going insane with query complexity.
+
+## Joins
+
+Add `joins` to your query to join other tables. Works with select and count.
+
+```json
+{
+ "action": "select",
+ "table": "items",
+ "columns": ["items.name", "categories.name"],
+ "joins": [
+ {
+ "table": "categories",
+ "on": "items.category_id = categories.id",
+ "type": "LEFT"
+ }
+ ]
+}
+```
+
+| Field | Type | Required | Default | Notes |
+|---------|--------|----------|-----------|------------------------------------|
+| `table` | string | yes | | table to join |
+| `on` | string | yes | | join condition |
+| `type` | string | no | `"INNER"` | `INNER`, `LEFT`, `RIGHT` |
+
+type may also be full but keep in mind that this is not support on MySQL/MariaDB backed servers so I advice against using it.
+
+You need read permission on every table you join. If you dont have access to a joined table the whole query should get rejected by jsonderulo and he will spank you.
+
+## Order By
+
+Sort your results:
+
+```json
+{
+ "order_by": [
+ { "column": "created_at", "direction": "DESC" },
+ { "column": "name", "direction": "ASC" }
+ ]
+}
+```
+
+| Field | Type | Required | Default |
+|-------------|--------|----------|---------|
+| `column` | string | yes | |
+| `direction` | string | no | `"ASC"` |
+
+You can sort by multiple columns. They apply in order.
diff --git a/query/insert.md b/query/insert.md
new file mode 100644
index 0000000..899f804
--- /dev/null
+++ b/query/insert.md
@@ -0,0 +1,50 @@
+# Insert
+
+[back to query](README.md) /// [home](../README.md)
+
+Add a new row to a table.
+
+## Request
+
+```json
+{
+ "action": "insert",
+ "table": "items",
+ "data": {
+ "name": "New Widget",
+ "category_id": 3,
+ "price": 24.99,
+ "status": "draft"
+ }
+}
+```
+
+## Fields
+
+| Field | Type | Required | Notes |
+|----------|--------|----------|------------------------------|
+| `action` | string | yes | must be `"insert"` |
+| `table` | string | yes | target table |
+| `data` | object | yes | column value pairs to insert |
+
+## Response
+
+```json
+{
+ "success": true,
+ "data": 42,
+ "rows_affected": 1
+}
+```
+
+`data` is the auto increment ID of the newly inserted row (`last_insert_id`).
+
+## Behaviors
+
+- columns you dont have write access to should cause jsonderulo to start stripping for you and remove the naughty stuff from `data`. if theres nothing left after stripping you get an error (cant insert nothing lol)
+- system columns should be auto populated:
+ - `created_by` set to your user ID
+ - `pinned_to` set to your user ID (unless you have `rwa` permission and explicitly set it to someone else)
+ - `created_at` handled by SQL defaults
+- if the table has auto generation rules configured (like auto generated codes or UUIDs) those should get applied automatically
+- you need write permission on the table. read only permission codes like `r`, `rg` and `ro` will give you a nice 403
diff --git a/query/select.md b/query/select.md
new file mode 100644
index 0000000..0b84dc8
--- /dev/null
+++ b/query/select.md
@@ -0,0 +1,67 @@
+# Select
+
+[back to query](README.md) /// [home](../README.md)
+
+Read rows from a table.
+
+## Request
+
+```json
+{
+ "action": "select",
+ "table": "items",
+ "columns": ["id", "name", "price"],
+ "filter": {
+ "and": [
+ { "column": "status", "op": "=", "value": "active" },
+ { "column": "price", "op": ">", "value": 10 }
+ ]
+ },
+ "joins": [
+ {
+ "table": "categories",
+ "on": "items.category_id = categories.id",
+ "type": "LEFT"
+ }
+ ],
+ "order_by": [
+ { "column": "name", "direction": "ASC" }
+ ],
+ "limit": 50,
+ "offset": 0
+}
+```
+
+## Fields
+
+| Field | Type | Required | Default | Notes |
+|------------|------------------|----------|----------------|--------------------------------------------------------------|
+| `action` | string | yes | | must be `"select"` |
+| `table` | string | yes | | target table |
+| `columns` | array of strings | no | all columns | which columns to return |
+| `where` | object | no | | simple key value filter (see [filters](filters.md)) |
+| `filter` | object | no | | structured filter with operators (see [filters](filters.md)) |
+| `joins` | array | no | | join other tables (see [filters](filters.md)) |
+| `order_by` | array | no | | sort results (see [filters](filters.md)) |
+| `limit` | integer | no | server default | max rows to return |
+| `offset` | integer | no | 0 | skip this many rows |
+
+## Response
+
+```json
+{
+ "success": true,
+ "data": [
+ { "id": 1, "name": "Widget", "price": 19.99 },
+ { "id": 2, "name": "Gadget", "price": 29.99 }
+ ]
+}
+```
+
+## Behaviors
+
+- if you dont specify `columns` you get all columns you have read access to
+- if you request columns youre not allowed to see they should get silently stripped and a `warning` should be included so you know json derulo censored you
+- `limit` should be auto applied if you dont set one (from config defaults). if you set one higher than your allowed max it gets capped and a `warning` tells you
+- ownership scoping should be automatically applied based on your permission level (see [ownership](../permissions/ownership.md))
+- you need read permission on every joined table in regards to what you want to join obv. otherwise your whole query gets spanked by jsonderulo!
diff --git a/query/update.md b/query/update.md
new file mode 100644
index 0000000..2c5b753
--- /dev/null
+++ b/query/update.md
@@ -0,0 +1,54 @@
+# Update
+
+[back to query](README.md) /// [home](../README.md)
+
+Modify existing rows in a table.
+
+## Request
+
+```json
+{
+ "action": "update",
+ "table": "items",
+ "data": {
+ "price": 29.99,
+ "status": "active"
+ },
+ "filter": {
+ "column": "id",
+ "op": "=",
+ "value": 42
+ }
+}
+```
+
+## Fields
+
+| Field | Type | Required | Notes |
+|----------|--------|----------|------------------------------|
+| `action` | string | yes | must be `"update"` |
+| `table` | string | yes | target table |
+| `data` | object | yes | column value pairs to update |
+| `where` | object | yes* | simple key value filter |
+| `filter` | object | yes* | structured filter |
+
+*at least one of `where` or `filter` is required. you cant do an unfiltered update.
+
+## Response
+
+```json
+{
+ "success": true,
+ "rows_affected": 1
+}
+```
+
+## Behaviors
+
+- a WHERE clause is **mandatory**. the server refuses to run an update without one. this is a safety thingy yk
+- columns you dont have write access to are silently stripped. `warning` is included if json derulo stripped for you
+- `last_modified_by` auto set to your user ID
+- `last_modified_at` is handled by sqls ON UPDATE trigger
+- ownership scoping applies: if you have `rwo` you can only update rows where `pinned_to` equals your user ID uness you have rwa. same deal with `rwg` for group scoping
+- limit is applied if set and capped to your max
+- auto generation rules apply on update too if configured for the update action