refactor query language
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
const FIELDS = [
|
||||
{ name: 'status', label: 'Status Code', type: 'number', operators: ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in'] },
|
||||
{ name: 'body', label: 'Response Body', type: 'string', operators: ['$eq', '$ne', '$contains', '$startsWith', '$endsWith', '$regex', '$exists'] },
|
||||
{ name: 'headers.*', label: 'Header', type: 'string', operators: ['$eq', '$ne', '$contains', '$startsWith', '$endsWith', '$regex', '$exists'] },
|
||||
{ name: '$select', label: 'CSS Selector', type: 'selector', operators: ['$eq', '$ne', '$contains', '$startsWith', '$endsWith', '$regex'] },
|
||||
{ name: '$json', label: 'JSON Path', type: 'jsonpath', operators: ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$contains', '$regex'] },
|
||||
{ name: '$responseTime', label: 'Response Time (ms)', type: 'number', operators: ['$eq', '$gt', '$gte', '$lt', '$lte'] },
|
||||
{ name: '$certExpiry', label: 'Cert Expiry (days)', type: 'number', operators: ['$eq', '$gt', '$gte', '$lt', '$lte'] },
|
||||
{ name: 'status', label: 'Status Code', type: 'number', operators: ['$eq', '$ne', '$gt', '$ge', '$lt', '$le'] },
|
||||
{ name: 'body', label: 'Response Body', type: 'string', operators: ['$eq', '$ne', '$co', '$sw', '$ew', '$re', '$exists'] },
|
||||
{ name: 'headers.*', label: 'Header', type: 'string', operators: ['$eq', '$ne', '$co', '$sw', '$ew', '$re', '$exists'] },
|
||||
{ name: '$html', label: 'HTML Selector', type: 'selector', operators: ['$eq', '$ne', '$co', '$sw', '$ew', '$re'] },
|
||||
{ name: '$json', label: 'JSON Path', type: 'jsonpath', operators: ['$eq', '$ne', '$gt', '$ge', '$lt', '$le', '$co', '$re'] },
|
||||
{ name: '$time', label: 'Response Time (ms)', type: 'number', operators: ['$eq', '$gt', '$ge', '$lt', '$le'] },
|
||||
{ name: '$certExpiry', label: 'Cert Expiry (days)', type: 'number', operators: ['$eq', '$gt', '$ge', '$lt', '$le'] },
|
||||
{ name: '$certIssuer', label: 'Cert Issuer', type: 'string', operators: ['$eq', '$ne', '$co', '$sw', '$ew', '$re'] },
|
||||
];
|
||||
|
||||
const OP_LABELS = {
|
||||
'$eq': '=', '$ne': '≠', '$gt': '>', '$gte': '≥', '$lt': '<', '$lte': '≤',
|
||||
'$contains': 'contains', '$startsWith': 'starts with', '$endsWith': 'ends with',
|
||||
'$regex': 'matches regex', '$exists': 'exists', '$in': 'in',
|
||||
'$eq': '=', '$ne': '≠', '$gt': '>', '$ge': '≥', '$lt': '<', '$le': '≤',
|
||||
'$co': 'contains', '$sw': 'starts with', '$ew': 'ends with',
|
||||
'$re': 'matches regex', '$exists': 'exists',
|
||||
};
|
||||
|
||||
class QueryBuilder {
|
||||
@@ -36,7 +37,7 @@ class QueryBuilder {
|
||||
// it instead of guessing.
|
||||
_defaultRules() {
|
||||
return [
|
||||
{ field: 'status', operator: '$gte', value: '200', headerName: '', selectorValue: '', jsonPath: '' },
|
||||
{ field: 'status', operator: '$ge', value: '200', headerName: '', selectorValue: '', jsonPath: '' },
|
||||
{ field: 'status', operator: '$lt', value: '300', headerName: '', selectorValue: '', jsonPath: '' },
|
||||
];
|
||||
}
|
||||
@@ -57,11 +58,11 @@ class QueryBuilder {
|
||||
|
||||
const parsedVal = this._parseValue(value, field, operator);
|
||||
|
||||
if (field === '$responseTime' || field === '$certExpiry') {
|
||||
if (field === '$time' || field === '$certExpiry' || field === '$certIssuer') {
|
||||
return { [field]: { [operator]: parsedVal } };
|
||||
}
|
||||
if (field === '$select') {
|
||||
return { '$select': { [rule.selectorValue || '*']: { [operator]: parsedVal } } };
|
||||
if (field === '$html') {
|
||||
return { '$html': { [rule.selectorValue || '*']: { [operator]: parsedVal } } };
|
||||
}
|
||||
if (field === '$json') {
|
||||
return { '$json': { [rule.jsonPath || '$']: { [operator]: parsedVal } } };
|
||||
@@ -78,15 +79,8 @@ class QueryBuilder {
|
||||
|
||||
_parseValue(value, field, operator) {
|
||||
if (operator === '$exists') return value !== 'false' && value !== '0';
|
||||
if (operator === '$in') {
|
||||
return value.split(',').map(v => {
|
||||
const trimmed = v.trim();
|
||||
const n = Number(trimmed);
|
||||
return isNaN(n) ? trimmed : n;
|
||||
});
|
||||
}
|
||||
const fieldDef = FIELDS.find(f => f.name === field);
|
||||
const numericOps = ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte'];
|
||||
const numericOps = ['$eq', '$ne', '$gt', '$ge', '$lt', '$le'];
|
||||
if (fieldDef?.type === 'number' || (numericOps.includes(operator) && fieldDef?.type === 'jsonpath')) {
|
||||
const n = Number(value);
|
||||
return isNaN(n) ? value : n;
|
||||
@@ -123,8 +117,8 @@ class QueryBuilder {
|
||||
_queryToRule(clause) {
|
||||
if (!clause || typeof clause !== 'object') return this._emptyRule();
|
||||
|
||||
if ('$responseTime' in clause || '$certExpiry' in clause) {
|
||||
const field = '$responseTime' in clause ? '$responseTime' : '$certExpiry';
|
||||
if ('$time' in clause || '$certExpiry' in clause || '$certIssuer' in clause) {
|
||||
const field = '$time' in clause ? '$time' : '$certIssuer' in clause ? '$certIssuer' : '$certExpiry';
|
||||
const ops = clause[field];
|
||||
if (typeof ops === 'object') {
|
||||
const [operator, value] = Object.entries(ops)[0] || ['$lt', ''];
|
||||
@@ -132,13 +126,13 @@ class QueryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
if ('$select' in clause) {
|
||||
const selMap = clause.$select;
|
||||
if ('$html' in clause) {
|
||||
const selMap = clause.$html;
|
||||
if (selMap && typeof selMap === 'object') {
|
||||
const [selectorValue, condition] = Object.entries(selMap)[0] || ['*', {}];
|
||||
if (condition && typeof condition === 'object') {
|
||||
const [operator, value] = Object.entries(condition)[0] || ['$eq', ''];
|
||||
return { field: '$select', operator, value: String(value), headerName: '', selectorValue, jsonPath: '' };
|
||||
return { field: '$html', operator, value: String(value), headerName: '', selectorValue, jsonPath: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +240,7 @@ class QueryBuilder {
|
||||
const operators = fieldDef.operators;
|
||||
|
||||
const needsHeader = rule.field === 'headers.*';
|
||||
const needsSelector = rule.field === '$select';
|
||||
const needsSelector = rule.field === '$html';
|
||||
const needsJsonPath = rule.field === '$json';
|
||||
|
||||
return `
|
||||
|
||||
+34
-31
@@ -65,7 +65,7 @@
|
||||
<a href="#ql-fields" class="nav-link">Fields</a>
|
||||
<a href="#ql-operators" class="nav-link">Operators</a>
|
||||
<a href="#ql-json" class="nav-link">$json</a>
|
||||
<a href="#ql-select" class="nav-link">$select</a>
|
||||
<a href="#ql-select" class="nav-link">$html</a>
|
||||
<a href="#ql-logical" class="nav-link">Logical</a>
|
||||
<a href="#ql-consider" class="nav-link">$consider</a>
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
<span class="k">"retry_interval_s"</span>: <span class="n">30</span>, <span class="c">// optional - seconds between retries. Default: 30</span>
|
||||
<span class="k">"resend_interval"</span>: <span class="n">10</span>, <span class="c">// optional - re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0</span>
|
||||
<span class="k">"cert_alert_days"</span>: <span class="n">0</span>, <span class="c">// optional - alert when TLS cert is within N days of expiry. 0 disables. Default: 0 (disabled)</span>
|
||||
<span class="k">"max_redirects"</span>: <span class="n">1</span>, <span class="c">// optional - follow up to N redirects (0-4). Default: 1</span>
|
||||
<span class="k">"channel_ids"</span>: [<span class="s">"<uuid>"</span>], <span class="c">// optional - notification channels to attach</span>
|
||||
<span class="k">"query"</span>: { ... } <span class="c">// optional - see Query Language below</span>
|
||||
}</pre>
|
||||
@@ -166,6 +167,7 @@
|
||||
<tr><td>retry_interval_s</td><td>number</td><td>Seconds between retries. Default: 30.</td></tr>
|
||||
<tr><td>resend_interval</td><td>number</td><td>If a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0.</td></tr>
|
||||
<tr><td>cert_alert_days</td><td>number</td><td>Fire a separate <code>cert</code> notification when the TLS certificate is within N days of expiring. 0 disables. Default: 0 (disabled).</td></tr>
|
||||
<tr><td>max_redirects</td><td>number</td><td>Follow up to N redirects (0-4). 0 = don't follow. Default: 1.</td></tr>
|
||||
<tr><td>channel_ids</td><td>string[]</td><td>Notification channel IDs to attach. See <a href="#notifications">Notifications</a>.</td></tr>
|
||||
<tr><td>query</td><td>object</td><td>Query conditions - see below</td></tr>
|
||||
</tbody>
|
||||
@@ -420,11 +422,13 @@ Content-Type: application/json
|
||||
<tbody>
|
||||
<tr><td>status</td><td>number</td><td>HTTP status code</td></tr>
|
||||
<tr><td>body</td><td>string</td><td>Full response body as text</td></tr>
|
||||
<tr><td>size</td><td>number</td><td>Response body size in bytes</td></tr>
|
||||
<tr><td>headers.<em>name</em></td><td>string</td><td>Response header, e.g. <code>headers.content-type</code></td></tr>
|
||||
<tr><td>$responseTime</td><td>number</td><td>Request latency in milliseconds</td></tr>
|
||||
<tr><td>$time</td><td>number</td><td>Request latency in milliseconds</td></tr>
|
||||
<tr><td>$certExpiry</td><td>number</td><td>Days until SSL certificate expires</td></tr>
|
||||
<tr><td>$certIssuer</td><td>string</td><td>Certificate authority name (e.g. "Let's Encrypt")</td></tr>
|
||||
<tr><td>$json</td><td>object</td><td>JSONPath expression against response body</td></tr>
|
||||
<tr><td>$select</td><td>object</td><td>CSS selector against response HTML</td></tr>
|
||||
<tr><td>$html</td><td>object</td><td>CSS selector against response HTML</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -437,14 +441,13 @@ Content-Type: application/json
|
||||
<tbody>
|
||||
<tr><td>$eq</td><td>Equal to</td><td>any</td></tr>
|
||||
<tr><td>$ne</td><td>Not equal to</td><td>any</td></tr>
|
||||
<tr><td>$gt / $gte</td><td>Greater than / or equal</td><td>number</td></tr>
|
||||
<tr><td>$lt / $lte</td><td>Less than / or equal</td><td>number</td></tr>
|
||||
<tr><td>$contains</td><td>String contains substring</td><td>string</td></tr>
|
||||
<tr><td>$startsWith</td><td>String starts with</td><td>string</td></tr>
|
||||
<tr><td>$endsWith</td><td>String ends with</td><td>string</td></tr>
|
||||
<tr><td>$regex</td><td>Matches regular expression</td><td>string</td></tr>
|
||||
<tr><td>$gt / $ge</td><td>Greater than / or equal</td><td>number</td></tr>
|
||||
<tr><td>$lt / $le</td><td>Less than / or equal</td><td>number</td></tr>
|
||||
<tr><td>$co</td><td>String contains substring</td><td>string</td></tr>
|
||||
<tr><td>$sw</td><td>String starts with</td><td>string</td></tr>
|
||||
<tr><td>$ew</td><td>String ends with</td><td>string</td></tr>
|
||||
<tr><td>$re</td><td>Matches regular expression</td><td>string</td></tr>
|
||||
<tr><td>$exists</td><td>Field is present and non-null</td><td>any</td></tr>
|
||||
<tr><td>$in</td><td>Value is in array</td><td>any</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="cb">
|
||||
@@ -454,8 +457,8 @@ Content-Type: application/json
|
||||
|
||||
<span class="c">// operator form</span>
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$lt"</span>: <span class="n">400</span> } }
|
||||
{ <span class="k">"body"</span>: { <span class="o">"$contains"</span>: <span class="s">"healthy"</span> } }
|
||||
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$contains"</span>: <span class="s">"application/json"</span> } }</pre>
|
||||
{ <span class="k">"body"</span>: { <span class="o">"$co"</span>: <span class="s">"healthy"</span> } }
|
||||
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$co"</span>: <span class="s">"application/json"</span> } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -472,17 +475,17 @@ Content-Type: application/json
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- $select -->
|
||||
<!-- $html -->
|
||||
<div id="ql-select" class="section">
|
||||
<h2>$select - CSS Selector</h2>
|
||||
<h2>$html - HTML Selector</h2>
|
||||
<p>Extract text content from an HTML response using a CSS selector. Useful for monitoring public pages without an API.</p>
|
||||
<div class="cb">
|
||||
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre><span class="c">// matches if <h1> text is exactly "Example Domain"</span>
|
||||
{ <span class="o">"$select"</span>: { <span class="s">"h1"</span>: { <span class="o">"$eq"</span>: <span class="s">"Example Domain"</span> } } }
|
||||
{ <span class="o">"$html"</span>: { <span class="s">"h1"</span>: { <span class="o">"$eq"</span>: <span class="s">"Example Domain"</span> } } }
|
||||
|
||||
<span class="c">// matches if status badge contains "operational"</span>
|
||||
{ <span class="o">"$select"</span>: { <span class="s">".status-badge"</span>: { <span class="o">"$contains"</span>: <span class="s">"operational"</span> } } }</pre>
|
||||
{ <span class="o">"$html"</span>: { <span class="s">".status-badge"</span>: { <span class="o">"$co"</span>: <span class="s">"operational"</span> } } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -492,7 +495,7 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre><span class="c">// $and - all conditions must match</span>
|
||||
{ <span class="o">"$and"</span>: [{ <span class="k">"status"</span>: <span class="n">200</span> }, { <span class="k">"body"</span>: { <span class="o">"$contains"</span>: <span class="s">"ok"</span> } }] }
|
||||
{ <span class="o">"$and"</span>: [{ <span class="k">"status"</span>: <span class="n">200</span> }, { <span class="k">"body"</span>: { <span class="o">"$co"</span>: <span class="s">"ok"</span> } }] }
|
||||
|
||||
<span class="c">// $or - any condition must match</span>
|
||||
{ <span class="o">"$or"</span>: [{ <span class="k">"status"</span>: <span class="n">200</span> }, { <span class="k">"status"</span>: <span class="n">204</span> }] }
|
||||
@@ -522,7 +525,7 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre><span class="c">// down if response time exceeds 2 seconds</span>
|
||||
{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">2000</span> } }
|
||||
{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$time"</span>: { <span class="o">"$gt"</span>: <span class="n">2000</span> } }
|
||||
|
||||
<span class="c">// down if cert expires in less than 7 days</span>
|
||||
{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$certExpiry"</span>: { <span class="o">"$lt"</span>: <span class="n">7</span> } }
|
||||
@@ -531,8 +534,8 @@ Content-Type: application/json
|
||||
{
|
||||
<span class="o">"$consider"</span>: <span class="s">"down"</span>,
|
||||
<span class="o">"$or"</span>: [
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$gte"</span>: <span class="n">500</span> } },
|
||||
{ <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">5000</span> } }
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$ge"</span>: <span class="n">500</span> } },
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$gt"</span>: <span class="n">5000</span> } }
|
||||
]
|
||||
}</pre>
|
||||
</div>
|
||||
@@ -544,7 +547,7 @@ Content-Type: application/json
|
||||
|
||||
<h3>Basic health endpoint</h3>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{ <span class="k">"status"</span>: <span class="n">200</span>, <span class="k">"body"</span>: { <span class="o">"$contains"</span>: <span class="s">"healthy"</span> } }</pre></div>
|
||||
<pre>{ <span class="k">"status"</span>: <span class="n">200</span>, <span class="k">"body"</span>: { <span class="o">"$co"</span>: <span class="s">"healthy"</span> } }</pre></div>
|
||||
|
||||
<h3>JSON API response shape</h3>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
@@ -557,7 +560,7 @@ Content-Type: application/json
|
||||
|
||||
<h3>Performance monitor (mark down if slow)</h3>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">1000</span> } }</pre></div>
|
||||
<pre>{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$time"</span>: { <span class="o">"$gt"</span>: <span class="n">1000</span> } }</pre></div>
|
||||
|
||||
<h3>Cert expiry alert</h3>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
@@ -565,10 +568,10 @@ Content-Type: application/json
|
||||
|
||||
<h3>Status page (HTML)</h3>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{ <span class="o">"$select"</span>: { <span class="s">".status-indicator"</span>: { <span class="o">"$eq"</span>: <span class="s">"All systems operational"</span> } } }</pre></div>
|
||||
<pre>{ <span class="o">"$html"</span>: { <span class="s">".status-indicator"</span>: { <span class="o">"$eq"</span>: <span class="s">"All systems operational"</span> } } }</pre></div>
|
||||
|
||||
<h3>Page down if a JSON queue is backed up</h3>
|
||||
<p>The killer demo: alert when a JSON field crosses a threshold. Uptime Kuma's keyword/json-query checks can't compose this - you'd need a script. Here it's one expression.</p>
|
||||
<p>Alert when a JSON field crosses a threshold. Most monitoring tools can't compose this without a script.</p>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="o">"$consider"</span>: <span class="s">"down"</span>,
|
||||
@@ -576,15 +579,15 @@ Content-Type: application/json
|
||||
}</pre></div>
|
||||
|
||||
<h3>Down if any signal looks bad</h3>
|
||||
<p>Compose multiple conditions with <code>$or</code> and flip the result with <code>$consider</code>. Each condition can mix status, body, headers, JSON, and CSS-selector checks freely.</p>
|
||||
<p>Compose multiple conditions with <code>$or</code> and flip the result with <code>$consider</code>.</p>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="o">"$consider"</span>: <span class="s">"down"</span>,
|
||||
<span class="o">"$or"</span>: [
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$gte"</span>: <span class="n">500</span> } },
|
||||
{ <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">3000</span> } },
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$ge"</span>: <span class="n">500</span> } },
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$gt"</span>: <span class="n">3000</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.healthy"</span>: { <span class="o">"$eq"</span>: <span class="n">false</span> } } },
|
||||
{ <span class="o">"$select"</span>: { <span class="s">".error-banner"</span>: { <span class="o">"$exists"</span>: <span class="n">true</span> } } }
|
||||
{ <span class="o">"$html"</span>: { <span class="s">".error-banner"</span>: { <span class="o">"$exists"</span>: <span class="n">true</span> } } }
|
||||
]
|
||||
}</pre></div>
|
||||
|
||||
@@ -594,8 +597,8 @@ Content-Type: application/json
|
||||
<pre>{
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$contains"</span>: <span class="s">"application/json"</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.version"</span>: { <span class="o">"$startsWith"</span>: <span class="s">"v2"</span> } } },
|
||||
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$co"</span>: <span class="s">"application/json"</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.version"</span>: { <span class="o">"$sw"</span>: <span class="s">"v2"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.db.connections"</span>: { <span class="o">"$lt"</span>: <span class="n">100</span> } } }
|
||||
]
|
||||
}</pre></div>
|
||||
@@ -611,7 +614,7 @@ Content-Type: application/json
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.env"</span>: { <span class="o">"$eq"</span>: <span class="s">"staging"</span> } } }
|
||||
] },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="k">"$responseTime"</span>: { <span class="o">"$lt"</span>: <span class="n">2000</span> } },
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$lt"</span>: <span class="n">2000</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.cache"</span>: { <span class="o">"$eq"</span>: <span class="s">"hit"</span> } } }
|
||||
] }
|
||||
]
|
||||
|
||||
@@ -375,7 +375,7 @@
|
||||
<svg class="w-5 h-5 text-brand" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-white mb-2">JSONPath & CSS selectors</h3>
|
||||
<p class="text-sm text-gray-400 leading-relaxed">Drill into JSON responses with <code class="text-brand font-mono text-xs">$json</code> or scrape any HTML page with <code class="text-brand font-mono text-xs">$select</code>. No API required.</p>
|
||||
<p class="text-sm text-gray-400 leading-relaxed">Drill into JSON responses with <code class="text-brand font-mono text-xs">$json</code> or scrape any HTML page with <code class="text-brand font-mono text-xs">$html</code>. No API required.</p>
|
||||
</div>
|
||||
|
||||
<div class="glow-card rounded-xl p-6">
|
||||
|
||||
Reference in New Issue
Block a user