Demo, show me the demo! Make your power points and blog posts, come to life! Demonstrating REST APIs vis cURL or Postman can get kind of tedious, or even boring.
If I had time, I would just build a local web app to show it off. But since I don’t have that much time, I’ll do the next best thing, have my AI Agent build the app for me!
The setup
I have an Oracle AI Database (26ai, but 19c, 21c, or 23ai would also be fine.) I have Oracle REST Data Services (ORDS) 25.4. I have a table, EMPLOYEES. It is registered for ORDS, to enable the AutoREST feature.
Our Oracle AI Database table REST APIs make working with your data, easy. I can GET
- all my rows
- one row
- some rows, based on some filters
- above, but sorted
This query parameter filter syntax is described and/or demonstrated:
My REST API is described here http://localhost:8080/ords/hr/open-api-catalog/employees/, and looks like this:
{
"openapi": "3.0.0",
"info": {
"title": "ORDS generated API for EMPLOYEES",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080/ords/hr/employees"
}
],
"paths": {
"/": {
"delete": {
"description": "Remove a record from EMPLOYEES",
"responses": {
"204": {
"description": "Deleted result."
}
},
"parameters": [
{
"name": "q",
"in": "query",
"schema": {
"type": "string"
},
"description": "JSON QBE parameter."
}
]
},
"get": {
"description": "Retrieve records from EMPLOYEES",
"responses": {
"200": {
"description": "The queried record.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
}
}
}
}
},
"parameters": [
{
"name": "q",
"in": "query",
"schema": {
"type": "string"
},
"description": "JSON QBE parameter."
}
]
},
"post": {
"description": "Create a new record on EMPLOYEES",
"responses": {
"201": {
"description": "The successfully created record.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
}
},
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
}
}
},
"/batchload": {
"post": {
"description": "Create new records on EMPLOYEES",
"responses": {
"200": {
"description": "The status of the processed records.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
}
}
},
"parameters": [],
"requestBody": {
"content": {
"text/csv": {
"schema": {}
}
}
}
}
},
"/{id}": {
"delete": {
"description": "Remove a record from EMPLOYEES",
"responses": {
"204": {
"description": "Deleted result."
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[^/]+$"
},
"description": "Primary key(s): employee_id"
}
]
},
"get": {
"description": "Retrieve a record from EMPLOYEES",
"responses": {
"200": {
"description": "The queried record.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[^/]+$"
},
"description": "Primary key(s): employee_id"
}
]
},
"put": {
"description": "Create or update a record on EMPLOYEES",
"responses": {
"200": {
"description": "The successfully updated record.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
},
"201": {
"description": "The successfully created record.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[^/]+$"
},
"description": "Primary key(s): employee_id"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/employees_ITEM"
}
}
}
}
}
}
},
"components": {
"schemas": {
"DATE": {
"type": "string",
"format": "date-time",
"pattern": "^\\d{4}-[01]\\d-[0123]\\dT[012]\\d:[0-5]\\d:[0-5]\\d(.\\d+)?(Z|([-+][012]\\d:[0-5]\\d))$"
},
"NUMBER": {
"type": "number"
},
"VARCHAR2": {
"type": "string"
},
"employees_ITEM": {
"properties": {
"commission_pct": {
"$ref": "#/components/schemas/NUMBER"
},
"department_id": {
"$ref": "#/components/schemas/NUMBER"
},
"email": {
"$ref": "#/components/schemas/VARCHAR2"
},
"employee_id": {
"x-ords-primary-key": "true",
"$ref": "#/components/schemas/NUMBER"
},
"first_name": {
"$ref": "#/components/schemas/VARCHAR2"
},
"hire_date": {
"$ref": "#/components/schemas/DATE"
},
"job_id": {
"$ref": "#/components/schemas/VARCHAR2"
},
"last_name": {
"$ref": "#/components/schemas/VARCHAR2"
},
"manager_id": {
"$ref": "#/components/schemas/NUMBER"
},
"phone_number": {
"$ref": "#/components/schemas/VARCHAR2"
},
"salary": {
"$ref": "#/components/schemas/NUMBER"
}
}
}
}
}
}My AI Agent Prompt

The Result
About 5 minutes later, I pretty much had a working app.
Pretty much, meaning I needed to step in for the following corrections in follow-up prompts:
- remind the Agent the ORDER BY clause was part of QBE, not part of the LIMIT clause
- enable scrolling on my results grid, and add pagination Next/Previous links

My app lets me toggle/input filters and sorting criteria, and it pretty prints the QBE JSON, and previews the complete ORDS URL for the GET request.
As an example, above I asked for ALL employees:
- having a last name starting with the letter ‘S’
- whose JOB_ID was ‘SA_REP’
- with a minimum salary of 7000
- with any COMMISSION PCT value (except for NULL)
- hired between Jan 1 2014 and June 1 2015
- ordered by LAST NAME desc
And our app took those inputs and decided that could be acquired via this QBE JSON payload on a query parameter:
{
"$and": [
{
"last_name": {
"$like": "S%"
}
},
{
"job_id": {
"$eq": "SA_REP"
}
},
{
"salary": {
"$gte": 7000
}
},
{
"hire_date": {
"$gte": {
"$date": "2014-01-01T00:00:00Z"
}
}
},
{
"hire_date": {
"$lte": {
"$date": "2015-06-01T23:59:59Z"
}
}
},
{
"commission_pct": {
"$notnull": null
}
}
],
"$orderby": {
"LAST_NAME": "desc"
}
}All the code
My web page source is below. I QA’d this for about 10 minutes, so I’m sure there’s more clean-up required, but it’s a great start.
The interesting part of the code above is the FILTER BUILDER section, that starts on line 700.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ORDS Query Filter Explorer — HR.EMPLOYEES</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #080a0d;
--bg1: #0e1218;
--bg2: #141b24;
--bg3: #1c2530;
--border: #232d3a;
--border2:#2e3d50;
--red: #c94236;
--red-hl: #e04535;
--cyan: #22d3ee;
--amber: #f59e0b;
--green: #22c55e;
--purple: #a78bfa;
--text: #dde5f0;
--text2: #8fa3b8;
--text3: #404f62;
--syne: 'Syne', sans-serif;
--body: 'DM Sans', sans-serif;
--mono: 'JetBrains Mono', monospace;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body {
font-family: var(--body);
background: var(--bg);
color: var(--text);
min-height: 100vh;
font-size: 14px;
line-height: 1.6;
}
/* ── SCROLLBAR ── */
::-webkit-scrollbar{width:4px;height:4px}
::-webkit-scrollbar-track{background:var(--bg1)}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}
/* ── HEADER ── */
.header {
background: var(--bg1);
border-bottom: 1px solid var(--border);
padding: 0 28px;
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.hb { display:flex; align-items:center; gap:14px }
.oracle-badge {
background: var(--red);
color: #fff;
font-family: var(--syne);
font-size: 10px;
font-weight: 800;
letter-spacing: .12em;
padding: 4px 9px;
text-transform: uppercase;
clip-path: polygon(0 0,calc(100% - 6px) 0,100% 100%,0 100%);
}
.header-title { font-family:var(--syne); font-size:15px; font-weight:700; letter-spacing:.02em }
.header-sub { font-family:var(--mono); font-size:10px; color:var(--text3); letter-spacing:.05em }
.endpoint-pill {
display:flex; align-items:center; gap:8px;
background:var(--bg2); border:1px solid var(--border);
padding:5px 12px; border-radius:2px;
font-family:var(--mono); font-size:11px; color:var(--cyan);
}
.dot-live { width:6px;height:6px;background:var(--green);border-radius:50%;animation:blink 2s ease-in-out infinite }
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
/* ── LAYOUT ── */
.app {
display: grid;
grid-template-columns: var(--sidebar-w, 320px) 4px 1fr;
grid-template-rows: auto 1fr;
height: calc(100vh - 58px);
}
/* ── RESIZE HANDLE ── */
.resizer {
grid-row: 1 / 3;
background: var(--border);
cursor: col-resize;
transition: background .15s;
position: relative;
z-index: 10;
}
.resizer:hover, .resizer.dragging { background: var(--cyan); }
.resizer::after {
content: '';
position: absolute;
inset: 0 -4px; /* wider invisible hit target */
}
/* ── SIDEBAR ── */
.sidebar {
grid-row: 1 / 3;
background: var(--bg1);
border-right: none;
overflow-y: auto;
padding: 20px 18px;
display: flex;
flex-direction: column;
gap: 18px;
min-width: 220px;
max-width: 600px;
}
.sec-label {
font-family:var(--syne); font-size:9px; font-weight:800;
letter-spacing:.14em; text-transform:uppercase; color:var(--text3);
display:flex; align-items:center; gap:8px; margin-bottom:10px;
}
.sec-label::after{content:'';flex:1;height:1px;background:var(--border)}
.fg { display:flex; flex-direction:column; gap:10px }
.fr { display:grid; grid-template-columns:1fr 1fr; gap:8px }
.field { display:flex; flex-direction:column; gap:3px }
.fl { font-family:var(--mono); font-size:9px; color:var(--text2); letter-spacing:.06em; text-transform:uppercase }
.field input, .field select {
background:var(--bg2); border:1px solid var(--border);
color:var(--text); padding:6px 9px;
font-family:var(--mono); font-size:12px;
border-radius:2px; outline:none; width:100%;
transition:border-color .15s, box-shadow .15s;
}
.field input:focus,.field select:focus{border-color:var(--cyan);box-shadow:0 0 0 2px rgba(34,211,238,.08)}
.field input::placeholder{color:var(--text3)}
.field select option{background:var(--bg2)}
.help{font-family:var(--mono);font-size:9px;color:var(--text3);line-height:1.5;margin-top:1px}
/* operator inline select */
.op-sel {
background:var(--bg2); border:1px solid var(--border);
color:var(--amber); padding:6px 5px;
font-family:var(--mono); font-size:10px;
border-radius:2px; outline:none; cursor:pointer; flex-shrink:0;
}
/* full-width stacked operator select */
.full-op {
width: 100%;
padding: 6px 9px;
font-size: 11px;
color: var(--amber);
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 2px;
outline: none;
cursor: pointer;
transition: border-color .15s;
}
.full-op:focus { border-color: var(--cyan); box-shadow: 0 0 0 2px rgba(34,211,238,.08); }
.full-op option { background: var(--bg2); }
.inline-pair { display:flex; gap:4px; align-items:stretch }
.inline-pair .op-sel { min-width:82px }
.inline-pair input { flex:1; min-width:0 }
/* toggle pairs */
.toggle {
display:flex; background:var(--bg2);
border:1px solid var(--border); border-radius:2px; overflow:hidden;
}
.tgl-btn {
flex:1; padding:5px 8px;
background:transparent; border:none;
color:var(--text2); font-family:var(--syne); font-size:10px;
font-weight:700; letter-spacing:.08em; cursor:pointer; transition:all .15s;
text-align:center;
}
.tgl-btn.on{background:var(--red);color:#fff}
/* commission row */
.comm-row { display:flex; gap:5px }
.comm-btn {
flex:1; padding:5px; background:var(--bg2);
border:1px solid var(--border); color:var(--text2);
font-family:var(--mono); font-size:9px; cursor:pointer;
transition:all .15s; text-align:center; border-radius:2px;
}
.comm-btn.on{border-color:var(--cyan);color:var(--cyan);background:rgba(34,211,238,.06)}
/* action buttons */
.btn-exec {
width:100%; padding:10px; background:var(--red); color:#fff;
border:none; font-family:var(--syne); font-size:12px; font-weight:800;
letter-spacing:.07em; text-transform:uppercase; cursor:pointer;
transition:all .15s; border-radius:2px;
}
.btn-exec:hover{background:var(--red-hl);box-shadow:0 4px 22px rgba(201,66,54,.4)}
.btn-reset {
width:100%; padding:7px; background:transparent; color:var(--text2);
border:1px solid var(--border); font-family:var(--syne); font-size:10px;
font-weight:700; letter-spacing:.07em; text-transform:uppercase;
cursor:pointer; transition:all .15s; border-radius:2px;
}
.btn-reset:hover{border-color:var(--border2);color:var(--text)}
/* ── QUERY PANEL ── */
.qpanel {
background:var(--bg1); border-bottom:1px solid var(--border);
padding:14px 22px; grid-column: 3;
}
.qpanel-inner { display:flex; gap:14px; align-items:flex-start }
.qblocks { flex:1; display:flex; flex-direction:column; gap:8px }
.qblock {
background:var(--bg); border:1px solid var(--border);
border-radius:2px; padding:10px 14px; position:relative;
}
.qblock.cyan{ border-left:3px solid var(--cyan) }
.qblock.amber{ border-left:3px solid var(--amber) }
.qbl { font-family:var(--mono); font-size:8px; letter-spacing:.12em; color:var(--text3); text-transform:uppercase; margin-bottom:5px }
.qtext {
font-family:var(--mono); font-size:11px; color:var(--cyan);
word-break:break-all; max-height:56px; overflow-y:auto;
white-space:pre-wrap; line-height:1.55;
}
.utext { font-family:var(--mono); font-size:10px; color:var(--text2); word-break:break-all; line-height:1.5 }
.utext .ub{color:var(--text3)} .utext .up{color:var(--amber)} .utext .uv{color:var(--cyan)}
.badges{ display:flex; flex-wrap:wrap; gap:5px; margin-top:2px }
.badge {
display:inline-flex; align-items:center; gap:4px;
background:var(--bg3); border:1px solid var(--border2);
padding:2px 7px; border-radius:2px;
font-family:var(--mono); font-size:9px;
}
.badge .bc{color:var(--amber)} .badge .bo{color:var(--text3)} .badge .bv{color:var(--cyan)}
.qactions { display:flex; flex-direction:column; gap:5px }
.cbtn {
padding:6px 12px; background:var(--bg2); border:1px solid var(--border);
color:var(--text2); font-family:var(--mono); font-size:10px;
cursor:pointer; transition:all .15s; border-radius:2px; white-space:nowrap;
}
.cbtn:hover{border-color:var(--cyan);color:var(--cyan)}
.cbtn.open:hover{border-color:var(--amber);color:var(--amber)}
/* ── MAIN ── */
.main {
padding: 14px 22px 0 22px;
overflow: hidden; /* main itself does NOT scroll */
display: flex;
flex-direction: column;
gap: 10px;
grid-column: 3;
min-height: 0; /* critical: lets flex children shrink below content size */
}
.results-bar { display:flex; align-items:center; justify-content:space-between; flex-shrink:0 }
.count-pill {
display:flex; align-items:center; gap:6px;
background:var(--bg2); border:1px solid var(--border);
padding:4px 12px; border-radius:2px;
font-family:var(--mono); font-size:11px;
}
.count-num{color:var(--amber);font-weight:500}
.page-loc{font-family:var(--mono);font-size:10px;color:var(--text2)}
/* table wrapper — fills remaining height, clips overflow */
.tbl-wrap {
background:var(--bg1); border:1px solid var(--border);
border-radius:2px;
display: flex;
flex-direction: column;
flex: 1 1 0; /* grow to fill, but never push beyond parent */
min-height: 0;
overflow: hidden;
margin-bottom: 14px;
}
/* scrollable zone — only the rows scroll, thead stays put */
.tbl-scroll {
overflow-x: auto;
overflow-y: auto;
flex: 1 1 0;
min-height: 0;
}
table { width:100%; border-collapse:collapse }
thead th {
background:var(--bg2); padding:9px 13px;
font-family:var(--syne); font-size:9px; font-weight:800;
letter-spacing:.11em; text-transform:uppercase;
color:var(--text3); border-bottom:1px solid var(--border);
white-space:nowrap; cursor:pointer; user-select:none;
transition:color .12s;
position: sticky; top: 0; z-index: 2; /* thead sticks to top of scroll area */
}
thead th:hover{color:var(--text2)}
thead th.sorted{color:var(--cyan)}
.si{margin-left:3px;font-size:9px;opacity:.5}
tbody tr{border-bottom:1px solid var(--border);transition:background .08s}
tbody tr:last-child{border-bottom:none}
tbody tr:hover{background:rgba(34,211,238,.025)}
tbody td{padding:8px 13px;font-family:var(--mono);font-size:11px;white-space:nowrap}
.tid{color:var(--text3)}
.tname{font-family:var(--body);font-size:13px;font-weight:500;color:var(--text)}
.tjob{
display:inline-block; background:var(--bg2);
border:1px solid var(--border); padding:1px 6px;
font-size:9px; letter-spacing:.05em; border-radius:2px; color:var(--text2);
}
.tsal{color:var(--green);font-weight:500}
.tdate{color:var(--text2)}
.tnull{color:var(--text3);font-style:italic;font-size:10px}
.tpct{color:var(--purple)}
/* pagination — pinned to bottom of tbl-wrap, never scrolls away */
.pg {
display:flex; align-items:center; justify-content:space-between;
padding:10px 14px; background:var(--bg2); border-top:1px solid var(--border);
flex-shrink: 0;
}
.pg-info{font-family:var(--mono);font-size:10px;color:var(--text2)}
.pg-btns{display:flex;gap:5px}
.pg-btn{
padding:4px 12px; background:var(--bg1);
border:1px solid var(--border); color:var(--text2);
font-family:var(--mono); font-size:10px; cursor:pointer;
transition:all .12s; border-radius:2px;
}
.pg-btn:hover:not(:disabled){border-color:var(--red);color:var(--red)}
.pg-btn:disabled{opacity:.25;cursor:not-allowed}
/* states */
.loading-st{display:flex;align-items:center;justify-content:center;padding:64px;gap:12px;color:var(--text2);font-family:var(--mono);font-size:12px}
.spin{width:16px;height:16px;border:2px solid var(--border2);border-top-color:var(--red);border-radius:50%;animation:rotate .65s linear infinite}
@keyframes rotate{to{transform:rotate(360deg)}}
.error-st{padding:28px;background:rgba(201,66,54,.04);border:1px solid rgba(201,66,54,.18);border-radius:2px}
.error-title{color:var(--red);font-family:var(--syne);font-size:13px;font-weight:700;margin-bottom:8px}
.error-body{font-family:var(--mono);font-size:11px;color:var(--text2);line-height:1.6}
.empty-st{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px;gap:6px;color:var(--text3)}
.empty-ico{font-size:28px;opacity:.3;margin-bottom:6px}
.welcome-st{display:flex;flex-direction:column;align-items:center;padding:72px 40px;gap:14px;text-align:center}
.welcome-ico{
width:60px;height:60px;background:var(--bg2);border:1px solid var(--border);
display:flex;align-items:center;justify-content:center;font-size:26px;
margin-bottom:4px; border-radius:2px;
}
.welcome-h{font-family:var(--syne);font-size:19px;font-weight:800;color:var(--text)}
.welcome-p{color:var(--text2);max-width:420px;font-size:13px;line-height:1.6}
.welcome-p code{font-family:var(--mono);font-size:11px;background:var(--bg2);padding:1px 5px;border-radius:2px;color:var(--cyan)}
/* toast */
.toast{
position:fixed;bottom:22px;right:22px;
background:var(--green);color:#fff;
padding:7px 16px;border-radius:2px;
font-family:var(--mono);font-size:11px;
z-index:9999;pointer-events:none;
transition:opacity .25s; opacity:0;
}
.toast.show{opacity:1}
</style>
</head>
<body>
<header class="header">
<div class="hb">
<div class="oracle-badge">Oracle ORDS</div>
<div>
<div class="header-title">AutoREST Query Filter Explorer</div>
<div class="header-sub">HR.EMPLOYEES · Dynamic <em>q=</em> Parameter Showcase</div>
</div>
</div>
<div class="endpoint-pill">
<span class="dot-live"></span>
localhost:8080/ords/hr/employees/
</div>
</header>
<div class="app">
<!-- ══════════════════════════════════════════════
SIDEBAR
══════════════════════════════════════════════ -->
<aside class="sidebar" id="sidebar">
<!-- Logic mode -->
<div>
<div class="sec-label">Top-level Logic</div>
<div class="fg">
<div class="toggle">
<button class="tgl-btn on" id="btn-and" onclick="setLogic('and')">AND</button>
<button class="tgl-btn" id="btn-or" onclick="setLogic('or')">OR</button>
</div>
<span class="help">AND = implicit object merge (default)<br>OR = $or:[…] array wrapper</span>
</div>
</div>
<!-- Identity -->
<div>
<div class="sec-label">Identity</div>
<div class="fg">
<div class="fr">
<div class="field">
<span class="fl">employee_id</span>
<input type="number" id="f-empid" placeholder="100–206" min="100" max="206">
</div>
<div class="field">
<span class="fl">emp_id op</span>
<select id="f-empop">
<option value="$eq">= eq</option>
<option value="$gt">> gt</option>
<option value="$gte">>= gte</option>
<option value="$lt">< lt</option>
<option value="$lte"><= lte</option>
</select>
</div>
</div>
<div class="fr">
<div class="field">
<span class="fl">department_id</span>
<input type="number" id="f-deptid" placeholder="e.g. 90">
</div>
<div class="field">
<span class="fl">manager_id</span>
<input type="number" id="f-mgrid" placeholder="e.g. 100">
</div>
</div>
</div>
</div>
<!-- Name -->
<div>
<div class="sec-label">Name Search</div>
<div class="fg">
<div class="field">
<span class="fl">first_name</span>
<select class="op-sel full-op" id="f-fnop">
<option value="$like">LIKE (wildcard)</option>
<option value="$eq">= eq (exact)</option>
<option value="$ne">≠ ne (not equal)</option>
</select>
<input type="text" id="f-fn" placeholder="%Steven%">
<span class="help">% is wildcard — e.g. %en% matches "Steven"</span>
</div>
<div class="field">
<span class="fl">last_name</span>
<select class="op-sel full-op" id="f-lnop">
<option value="$like">LIKE (wildcard)</option>
<option value="$eq">= eq (exact)</option>
<option value="$ne">≠ ne (not equal)</option>
</select>
<input type="text" id="f-ln" placeholder="K%">
</div>
</div>
</div>
<!-- Job -->
<div>
<div class="sec-label">Job</div>
<div class="fg">
<div class="field">
<span class="fl">job_id</span>
<select id="f-jobid">
<option value="">— any —</option>
<optgroup label="Accounting">
<option>AC_ACCOUNT</option><option>AC_MGR</option>
</optgroup>
<optgroup label="Administration">
<option>AD_ASST</option><option>AD_PRES</option><option>AD_VP</option>
</optgroup>
<optgroup label="Finance">
<option>FI_ACCOUNT</option><option>FI_MGR</option>
</optgroup>
<optgroup label="Human Resources">
<option>HR_REP</option>
</optgroup>
<optgroup label="IT">
<option>IT_PROG</option>
</optgroup>
<optgroup label="Marketing">
<option>MK_MAN</option><option>MK_REP</option>
</optgroup>
<optgroup label="Public Relations">
<option>PR_REP</option>
</optgroup>
<optgroup label="Purchasing">
<option>PU_CLERK</option><option>PU_MAN</option>
</optgroup>
<optgroup label="Sales">
<option>SA_MAN</option><option>SA_REP</option>
</optgroup>
<optgroup label="Shipping">
<option>SH_CLERK</option><option>ST_CLERK</option><option>ST_MAN</option>
</optgroup>
<option value="__PAT__">Custom LIKE pattern…</option>
</select>
</div>
<div class="field" id="job-pat-wrap" style="display:none">
<span class="fl">job_id LIKE pattern</span>
<input type="text" id="f-jobpat" placeholder="%CLERK">
<span class="help">e.g. %CLERK — matches all clerks</span>
</div>
</div>
</div>
<!-- Salary -->
<div>
<div class="sec-label">Salary</div>
<div class="fg">
<div class="fr">
<div class="field">
<span class="fl">min ($gte)</span>
<input type="number" id="f-salmin" placeholder="2100" min="2100" max="24000">
</div>
<div class="field">
<span class="fl">max ($lte)</span>
<input type="number" id="f-salmax" placeholder="24000" min="2100" max="24000">
</div>
</div>
<span class="help">Dataset range: $2,100 – $24,000</span>
</div>
</div>
<!-- Hire Date -->
<div>
<div class="sec-label">Hire Date</div>
<div class="fg">
<div class="field">
<span class="fl">hired after ($gte)</span>
<input type="date" id="f-hfrom" min="2011-01-13" max="2018-04-21">
</div>
<div class="field">
<span class="fl">hired before ($lte)</span>
<input type="date" id="f-hto" min="2011-01-13" max="2018-04-21">
</div>
<span class="help">Uses ORDS date wrapper: {"$date":"ISO-Z"}</span>
</div>
</div>
<!-- Commission -->
<div>
<div class="sec-label">Commission PCT</div>
<div class="comm-row">
<button class="comm-btn on" id="cb-all" onclick="setComm('all')">All</button>
<button class="comm-btn" id="cb-has" onclick="setComm('has')">Has ($notnull)</button>
<button class="comm-btn" id="cb-none" onclick="setComm('none')">None ($null)</button>
</div>
</div>
<!-- Display -->
<div>
<div class="sec-label">Display & Sort</div>
<div class="fg">
<div class="fr">
<div class="field">
<span class="fl">limit</span>
<select id="f-limit">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="field">
<span class="fl">order by</span>
<select id="f-orderby">
<option value="">— default —</option>
<option value="EMPLOYEE_ID">Employee ID</option>
<option value="LAST_NAME">Last Name</option>
<option value="SALARY">Salary</option>
<option value="HIRE_DATE">Hire Date</option>
<option value="DEPARTMENT_ID">Dept ID</option>
</select>
</div>
</div>
<div class="toggle">
<button class="tgl-btn on" id="btn-asc" onclick="setOrd('ASC')">ASC ↑</button>
<button class="tgl-btn" id="btn-desc" onclick="setOrd('DESC')">DESC ↓</button>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px">
<button class="btn-exec" onclick="runQuery()">▶ Execute Query</button>
<button class="btn-reset" onclick="resetAll()">↺ Reset All Filters</button>
</div>
</aside>
<div class="resizer" id="resizer"></div>
<!-- ══════════════════════════════════════════════
QUERY PREVIEW PANEL
══════════════════════════════════════════════ -->
<div class="qpanel">
<div class="qpanel-inner">
<div class="qblocks">
<div class="qblock cyan">
<div class="qbl">q= filter parameter (ORDS JSON grammar)</div>
<div class="qtext" id="qprev">{ }</div>
</div>
<div class="qblock amber">
<div class="qbl">Full ORDS Request URL</div>
<div class="utext" id="uprev"><span class="ub">http://localhost:8080/ords/hr/employees/</span></div>
</div>
<div class="badges" id="badges-row"></div>
</div>
<div class="qactions">
<button class="cbtn" onclick="copyQ()">⎘ Copy q=</button>
<button class="cbtn" onclick="copyURL()">⎘ Copy URL</button>
<button class="cbtn open" onclick="openURL()">↗ Open URL</button>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════
MAIN RESULTS
══════════════════════════════════════════════ -->
<main class="main">
<div class="results-bar">
<div class="count-pill">
<span class="count-num" id="rcount">—</span>
<span>rows returned</span>
</div>
<span class="page-loc" id="ploc"></span>
</div>
<div class="tbl-wrap" id="tbl-wrap">
<div class="welcome-st">
<div class="welcome-ico">⚡</div>
<div class="welcome-h">ORDS AutoREST Query Filter Explorer</div>
<p class="welcome-p">
Build a filter using the controls on the left, then click <strong>Execute Query</strong>.<br>
Watch the <code>q=</code> parameter assemble in real time — showcasing how a single
AutoREST endpoint handles every query shape without any custom SQL.
</p>
</div>
</div>
</main>
</div>
<div class="toast" id="toast"></div>
<script>
// ─────────────────────────────────────────────
// STATE
// ─────────────────────────────────────────────
const BASE = 'http://localhost:8080/ords/hr/employees/';
const S = {
logic: 'and',
ord: 'ASC',
comm: 'all',
offset: 0
};
// ─────────────────────────────────────────────
// WIRE UP LIVE PREVIEW
// ─────────────────────────────────────────────
document.querySelectorAll('input, select').forEach(el => {
el.addEventListener('input', livePreview);
el.addEventListener('change', livePreview);
});
document.getElementById('f-jobid').addEventListener('change', function() {
const wrap = document.getElementById('job-pat-wrap');
wrap.style.display = this.value === '__PAT__' ? 'flex' : 'none';
if (this.value === '__PAT__') wrap.style.flexDirection = 'column';
});
// ─────────────────────────────────────────────
// TOGGLE HELPERS
// ─────────────────────────────────────────────
function setLogic(v) {
S.logic = v;
q('btn-and').classList.toggle('on', v==='and');
q('btn-or').classList.toggle('on', v==='or');
livePreview();
}
function setOrd(v) {
S.ord = v;
q('btn-asc').classList.toggle('on', v==='ASC');
q('btn-desc').classList.toggle('on', v==='DESC');
livePreview();
}
function setComm(v) {
S.comm = v;
['all','has','none'].forEach(id => q('cb-'+id).classList.toggle('on', id===v));
livePreview();
}
function q(id) { return document.getElementById(id) }
function val(id) { return q(id).value.trim() }
// ─────────────────────────────────────────────
// FILTER BUILDER
// ─────────────────────────────────────────────
function collectFilters() {
const fs = [];
const empid = val('f-empid');
if (empid) fs.push({ col:'employee_id', op:val('f-empop'), v:+empid, lbl:`employee_id ${val('f-empop')} ${empid}` });
const deptid = val('f-deptid');
if (deptid) fs.push({ col:'department_id', op:'$eq', v:+deptid, lbl:`department_id = ${deptid}` });
const mgrid = val('f-mgrid');
if (mgrid) fs.push({ col:'manager_id', op:'$eq', v:+mgrid, lbl:`manager_id = ${mgrid}` });
const fn = val('f-fn');
if (fn) fs.push({ col:'first_name', op:val('f-fnop'), v:fn, lbl:`first_name ${val('f-fnop')} "${fn}"` });
const ln = val('f-ln');
if (ln) fs.push({ col:'last_name', op:val('f-lnop'), v:ln, lbl:`last_name ${val('f-lnop')} "${ln}"` });
const jobid = val('f-jobid');
if (jobid && jobid !== '__PAT__') {
fs.push({ col:'job_id', op:'$eq', v:jobid, lbl:`job_id = "${jobid}"` });
} else if (jobid === '__PAT__') {
const pat = val('f-jobpat');
if (pat) fs.push({ col:'job_id', op:'$like', v:pat, lbl:`job_id LIKE "${pat}"` });
}
const salmin = val('f-salmin');
if (salmin) fs.push({ col:'salary', op:'$gte', v:+salmin, lbl:`salary >= ${salmin}` });
const salmax = val('f-salmax');
if (salmax) fs.push({ col:'salary', op:'$lte', v:+salmax, lbl:`salary <= ${salmax}` });
const hfrom = val('f-hfrom');
if (hfrom) fs.push({ col:'hire_date', op:'$gte', v:{ $date: hfrom+'T00:00:00Z' }, lbl:`hire_date >= ${hfrom}`, isDate:true });
const hto = val('f-hto');
if (hto) fs.push({ col:'hire_date', op:'$lte', v:{ $date: hto+'T23:59:59Z' }, lbl:`hire_date <= ${hto}`, isDate:true });
if (S.comm === 'has') fs.push({ col:'commission_pct', op:'$notnull', v:null, lbl:'commission_pct IS NOT NULL', isNull:true });
if (S.comm === 'none') fs.push({ col:'commission_pct', op:'$null', v:null, lbl:'commission_pct IS NULL', isNull:true });
return fs;
}
function buildQ(fs) {
const orderby = val('f-orderby');
// Build atomic predicate objects
function pred(f) {
if (f.op === '$null') return { [f.col]: { $null: null } };
if (f.op === '$notnull') return { [f.col]: { $notnull: null } };
return { [f.col]: { [f.op]: f.v } };
}
// $orderby clause — lives inside q= JSON per ORDS grammar
const orderClause = orderby
? { $orderby: { [orderby]: S.ord.toLowerCase() } }
: null;
if (!fs.length) {
// No filters — return just the orderby clause (or null if nothing at all)
return orderClause || null;
}
const preds = fs.map(pred);
let qObj;
if (S.logic === 'or') {
qObj = { $or: preds };
} else {
// AND: if any column is repeated we must use explicit $and array
const cols = fs.map(f => f.col);
const hasDup = cols.some((c,i) => cols.indexOf(c) !== i);
qObj = hasDup ? { $and: preds } : Object.assign({}, ...preds);
}
// Merge $orderby into the top-level q object
if (orderClause) Object.assign(qObj, orderClause);
return qObj;
}
// ─────────────────────────────────────────────
// PARAMS & URL
// ─────────────────────────────────────────────
function buildParams(qObj, offset) {
const ps = [];
const limit = +val('f-limit') || 25;
if (qObj) ps.push(['q', JSON.stringify(qObj)]);
ps.push(['limit', limit]);
if (offset > 0) ps.push(['offset', offset]);
// $orderby lives inside q= per ORDS grammar — no separate URL params needed
return ps;
}
function buildURL(qObj, offset) {
const ps = buildParams(qObj, offset);
if (!ps.length) return BASE;
return BASE + '?' + ps.map(([k,v]) => `${k}=${encodeURIComponent(v)}`).join('&');
}
// ─────────────────────────────────────────────
// LIVE PREVIEW
// ─────────────────────────────────────────────
function livePreview() {
const fs = collectFilters();
const qObj = buildQ(fs);
const pretty = qObj ? JSON.stringify(qObj, null, 2) : '{ }';
q('qprev').textContent = pretty;
// URL with colored spans
const ps = buildParams(qObj, 0);
if (ps.length <= 1 && !qObj) {
q('uprev').innerHTML = `<span class="ub">${BASE}</span>`;
} else {
let html = `<span class="ub">${BASE}?</span>`;
html += ps.map(([k,v]) =>
`<span class="up">${k}</span><span class="ub">=</span><span class="uv">${encodeURIComponent(v)}</span>`
).join('<span class="ub">&</span>');
q('uprev').innerHTML = html;
}
// Active filter badges
q('badges-row').innerHTML = fs.map(f =>
`<span class="badge"><span class="bc">${f.col}</span><span class="bo"> ${f.op.replace('$','')} </span><span class="bv">${f.isNull ? 'NULL' : (f.isDate ? f.lbl.split(/[<>]=? /)[1] : String(f.v).substring(0,24))}</span></span>`
).join('');
}
// ─────────────────────────────────────────────
// QUERY EXECUTION
// ─────────────────────────────────────────────
async function runQuery(offset=0) {
S.offset = offset;
livePreview();
const fs = collectFilters();
const qObj = buildQ(fs);
const url = buildURL(qObj, offset);
const limit = +val('f-limit') || 25;
q('tbl-wrap').innerHTML = `<div class="loading-st"><div class="spin"></div><span>Calling ORDS AutoREST endpoint…</span></div>`;
q('rcount').textContent = '…';
q('ploc').textContent = '';
try {
const res = await fetch(url, { headers:{ Accept:'application/json' } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const data = await res.json();
renderTable(data, offset, limit);
} catch(err) {
const isCors = err.message.toLowerCase().includes('fetch') || err.message.toLowerCase().includes('failed');
q('tbl-wrap').innerHTML = `
<div class="error-st">
<div class="error-title">⚠ Request Failed</div>
<div class="error-body">
${err.message}<br><br>
<strong>Attempted URL:</strong><br>
<span style="color:var(--amber)">${escHtml(url)}</span>
${isCors ? `<br><br><strong>CORS tip:</strong> Serve this file from a local server (e.g. <code>python -m http.server 3000</code>) so the browser allows cross-origin requests to localhost:8080. Alternatively confirm ORDS has <code>cors.allowed.origins=*</code> or the matching origin in its config.` : ''}
</div>
</div>`;
q('rcount').textContent = '—';
}
}
function renderTable(data, offset, limit) {
const items = data.items || [];
const hasMore = !!data.hasMore;
const count = data.count ?? items.length;
q('rcount').textContent = count;
if (items.length) {
q('ploc').textContent = `rows ${offset+1}–${offset+items.length}${hasMore?' (more available)':''}`;
}
if (!items.length) {
q('tbl-wrap').innerHTML = `
<div class="empty-st">
<div class="empty-ico">🔍</div>
<p style="font-family:var(--mono);font-size:12px">No employees matched the current filters.</p>
<p style="font-family:var(--mono);font-size:10px;margin-top:4px;color:var(--text3)">Relax one or more conditions and try again.</p>
</div>`;
return;
}
const cols = [
{ k:'employee_id', h:'EMP_ID', fn: v => `<td class="tid">${v}</td>` },
{ k:'first_name', h:'FIRST', fn: v => `<td class="tname">${v??'<span class="tnull">null</span>'}</td>` },
{ k:'last_name', h:'LAST', fn: v => `<td class="tname">${v??'<span class="tnull">null</span>'}</td>` },
{ k:'email', h:'EMAIL', fn: v => `<td>${v??''}</td>` },
{ k:'job_id', h:'JOB_ID', fn: v => `<td><span class="tjob">${v}</span></td>` },
{ k:'department_id', h:'DEPT', fn: v => v!=null?`<td class="tid">${v}</td>`:`<td><span class="tnull">null</span></td>` },
{ k:'salary', h:'SALARY', fn: v => `<td class="tsal">$${Number(v).toLocaleString()}</td>` },
{ k:'commission_pct',h:'COMM%', fn: v => v!=null?`<td class="tpct">${(v*100).toFixed(0)}%</td>`:`<td><span class="tnull">—</span></td>` },
{ k:'hire_date', h:'HIRE_DATE',fn: v => `<td class="tdate">${fmtDate(v)}</td>` },
{ k:'manager_id', h:'MGR', fn: v => v!=null?`<td class="tid">${v}</td>`:`<td><span class="tnull">—</span></td>` },
];
const thead = '<tr>' + cols.map(c =>
`<th onclick="clickSort('${c.k}')" id="th-${c.k}">${c.h}<span class="si">↕</span></th>`
).join('') + '</tr>';
const tbody = items.map(row =>
'<tr>' + cols.map(c => c.fn(row[c.k])).join('') + '</tr>'
).join('');
const pg = `
<div class="pg">
<span class="pg-info">Page ${Math.floor(offset/limit)+1} · ${items.length} row(s) this page</span>
<div class="pg-btns">
<button class="pg-btn" onclick="runQuery(${Math.max(0,offset-limit)})" ${offset===0?'disabled':''}>← Prev</button>
<button class="pg-btn" onclick="runQuery(${offset+limit})" ${!hasMore?'disabled':''}>Next →</button>
</div>
</div>`;
q('tbl-wrap').innerHTML = `<div class="tbl-scroll"><table><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>${pg}`;
// Mark sorted column
const ob = val('f-orderby');
if (ob) {
const el = document.getElementById('th-'+ob.toLowerCase());
if (el) { el.classList.add('sorted'); el.querySelector('.si').textContent = S.ord==='ASC'?' ↑':' ↓'; }
}
}
function clickSort(col) {
const current = val('f-orderby');
const colUC = col.toUpperCase();
if (current === colUC) {
setOrd(S.ord==='ASC'?'DESC':'ASC');
} else {
q('f-orderby').value = colUC;
setOrd('ASC');
}
runQuery(0);
}
// ─────────────────────────────────────────────
// COPY / OPEN
// ─────────────────────────────────────────────
function copyQ() {
const qObj = buildQ(collectFilters());
navigator.clipboard.writeText(qObj ? JSON.stringify(qObj) : '').then(()=>toast('q= parameter copied!'));
}
function copyURL() {
navigator.clipboard.writeText(buildURL(buildQ(collectFilters()),0)).then(()=>toast('URL copied!'));
}
function openURL() {
window.open(buildURL(buildQ(collectFilters()),0), '_blank');
}
// ─────────────────────────────────────────────
// RESET
// ─────────────────────────────────────────────
function resetAll() {
['f-empid','f-deptid','f-mgrid','f-fn','f-ln','f-jobpat','f-salmin','f-salmax','f-hfrom','f-hto']
.forEach(id => q(id).value = '');
q('f-empop').value = '$eq';
q('f-fnop').value = '$like';
q('f-lnop').value = '$like';
q('f-jobid').value = '';
q('f-limit').value = '25';
q('f-orderby').value = '';
q('job-pat-wrap').style.display = 'none';
setLogic('and');
setOrd('ASC');
setComm('all');
livePreview();
}
// ─────────────────────────────────────────────
// UTILS
// ─────────────────────────────────────────────
function fmtDate(v) {
if (!v) return '<span class="tnull">—</span>';
try {
const d = new Date(v);
return d.toLocaleDateString('en-US',{year:'numeric',month:'short',day:'numeric'});
} catch { return v; }
}
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
let toastTimer;
function toast(msg) {
const t = q('toast');
t.textContent = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(()=>t.classList.remove('show'), 2200);
}
// ─────────────────────────────────────────────
// SIDEBAR RESIZE
// ─────────────────────────────────────────────
(function() {
const resizer = document.getElementById('resizer');
const app = resizer.closest('.app');
const sidebar = document.getElementById('sidebar');
const MIN_W = 220;
const MAX_W = 600;
let dragging = false;
let startX, startW;
resizer.addEventListener('mousedown', e => {
dragging = true;
startX = e.clientX;
startW = sidebar.getBoundingClientRect().width;
resizer.classList.add('dragging');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
const newW = Math.min(MAX_W, Math.max(MIN_W, startW + (e.clientX - startX)));
app.style.setProperty('--sidebar-w', newW + 'px');
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
resizer.classList.remove('dragging');
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
})();
// ─────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────
livePreview();
</script>
</body>
</html>
My last ORDS post on filtering GET requests?
One would think I had done it all, but nope. We have some really cool stuff cooking in version 26.1 about to drop in a few weeks. Think…VECTOR SEARCH.