Files
pocketbase/ui/src/collections/collectionViewQueryTab.js
2026-04-22 16:04:57 +03:00

197 lines
7.4 KiB
JavaScript

const TEST_REQUEST_KEY = "test_view_query";
export function collectionViewQueryTab(upsertData) {
const uniqueId = "query_" + app.utils.randomString();
// dprint-ignore
const autocomplete = [
"SELECT", "FROM", "WHERE", "LEFT JOIN", "INNER JOIN", "ON",
"GROUP BY", "HAVING", "ORDER BY", "LIMIT", "OFFSET", "AS",
"WITH", "NOT", "IN", "EXISTS", "LIKE", "CAST",
];
const local = store({
testRecords: [],
testError: "",
isTesting: false,
});
async function dryRunViewQuery(query) {
local.isTesting = true;
local.testRecords = [];
// reset form errors related to the query
if (app.store.errors?.viewQuery || app.store.errors?.fields) {
delete app.store.errors.viewQuery;
delete app.store.errors.fields;
}
if (!query) {
local.testError = "";
local.isTesting = false;
return;
}
try {
// @todo replace with SDK method
const result = await app.pb.send("/api/collections/meta/dry-run-view", {
method: "POST",
body: { "query": query },
requestKey: TEST_REQUEST_KEY,
});
if (upsertData.collection?.id) {
// replace the collection meta fields
local.testRecords = result.sample.map((r) => {
r.collectionId = upsertData.collection?.id;
r.collectionName = upsertData.collection?.name;
return r;
});
} else {
local.testRecords = result.sample;
}
local.testError = "";
local.isTesting = false;
} catch (err) {
if (!err.isAbort) {
local.testError = err.message || "Invalid query.";
local.isTesting = false;
}
}
}
let testDebounceId;
const watchers = [
watch(() => upsertData.collection?.viewQuery, (newQuery) => {
clearTimeout(testDebounceId);
testDebounceId = setTimeout(() => dryRunViewQuery(newQuery), 200);
}),
];
return t.div(
{
pbEvent: "collectionViewQueryTabContent",
className: "collection-tab-content collection-view-query-tab-content",
onunmount: () => {
clearTimeout(testDebounceId);
app.pb.cancelRequest(TEST_REQUEST_KEY);
watchers.forEach((w) => w?.unwatch());
},
},
t.div(
{ className: "grid" },
t.div(
{ className: "col-12" },
t.div(
{ className: "txt-right txt-sm m-b-10" },
t.button(
{
type: "button",
className: "txt-bold link-hint",
"html-popovertarget": uniqueId + "caveats_dropdown",
},
() => "Query caveats",
),
),
t.div(
{
id: uniqueId + "caveats_dropdown",
className: "dropdown sm query-caveats-dropdown",
popover: "auto",
},
t.ul(
null,
t.li(null, "Wildcard columns (*) are not supported."),
t.li(
null,
"The query must have a unique ",
t.code(null, "id"),
" column.",
t.br(),
"If your query doesn't have a suitable one, you can use the universal ",
t.code(null, "(ROW_NUMBER() OVER()) as id"),
".",
),
t.li(
null,
"Expressions must be aliased with a valid formatted field name, e.g. ",
t.code(null, "MAX(balance) as maxBalance"),
".",
),
t.li(
null,
"Combined/multi-spaced expressions must be wrapped in parenthesis, e.g. ",
t.code(null, "(MAX(balance) + 1) as maxBalance"),
".",
),
t.li(
null,
"UNION expressions are supported but the entire query must be wrapped in parenthesis.",
),
),
),
t.div(
{ className: "field" },
t.label(
{ htmlFor: uniqueId + ".viewQuery" },
t.span({ className: "txt" }, "Select query"),
t.span(
{
hidden: () => !local.testError,
className: "query-state",
ariaDescription: app.attrs.tooltip("Invalid query", "left"),
},
t.i({ className: "ri-error-warning-fill txt-danger", ariaHidden: true }),
),
t.span(
{
hidden: () => !!local.testError,
className: "query-state",
ariaDescription: app.attrs.tooltip("Valid query", "left"),
},
t.i({ className: "ri-checkbox-circle-fill txt-success", ariaHidden: true }),
),
),
app.components.codeEditor({
id: uniqueId + ".viewQuery",
name: "viewQuery",
language: "sql",
required: true,
autocomplete: autocomplete,
className: "inline-error",
value: () => upsertData.collection.viewQuery || "",
oninput: (newVal) => {
upsertData.collection.viewQuery = newVal;
},
}),
),
),
t.div(
{ className: "col-12" },
t.p(
{ className: "txt-sm txt-bold" },
"Sample output:",
),
t.div(
{ className: "view-query-sample-wrapper" },
t.span({ hidden: () => !local.isTesting, className: "loader sm" }),
app.components.codeBlock({
language: () => local.testError ? "plain" : "js",
className: () => `view-query-sample ${local.testError ? "txt-danger" : ""}`,
value: () => {
if (local.testRecords?.length) {
return JSON.stringify(local.testRecords, null, 2);
}
return local.testError || "N/A";
},
}),
),
),
),
);
}