Blog/Components/Pages/Query.razor.js 8.6 K · 272 lines · raw · history

1 import {getById, writeError, writeDebug, h, t} from '/common.module.js'
2 import {DACTAL} from "/dactal.js"
3 import {query as jsonql} from "/jsonql-js/index.js"
4
5 const enterEvent = new KeyboardEvent('keydown', {
6 key: 'Enter',
7 code: 'Enter',
8 keyCode: 13,
9 which: 13,
10 bubbles: true,
11 cancelable: true
12 });
13
14 const defaultQueries = {
15 jsonql: `SELECT *
16 FROM personen AS p
17 WHERE 'EenhoofdigOuderlijkGezag' IN p.gezag[].type`,
18 dactal: "personen.gezag:type=EenhoofdigOuderlijkGezag",
19 }
20
21 const DB_NAME = "mb-storage"
22 const HISTORY_STORE = "queryHistory"
23 const FAVORITES_STORE = "queryFavorites"
24 const HISTORY_LIMIT = 50
25
26 let dactal
27 let db
28 let input
29 let lang
30
31 export async function onLoad() {
32 input = getById("input")
33 lang = getById("lang")
34 const favoriteButton = getById("favoriteButton")
35 dactal = new DACTAL()
36
37 db = await openDB()
38 await renderHistory()
39 await renderFavorites()
40
41 fetch("/brp.json").then(async res => {
42 const cdata = await res.json()
43 dactal.load(cdata, "personen")
44 writeDebug("Loaded brp data")
45 await runQuery(input.value, lang.value)
46 })
47
48 input.onkeydown = async (e) => {
49 if (e.key === "Enter" && !e.shiftKey) {
50 e.preventDefault()
51 await runQuery(input.value, lang.value)
52 // Only real keystrokes count as a run worth remembering, not the
53 // synthetic Enter dispatched below to trigger the initial query.
54 if (e.isTrusted) {
55 await recordHistory(lang.value, input.value)
56 }
57 }
58 }
59
60 lang.onchange = async () => {
61 input.value = defaultQueries[lang.value]
62 await runQuery(input.value, lang.value)
63 }
64
65 favoriteButton.onclick = async () => {
66 const name = await getFavoriteName()
67 if (name) {
68 await addFavorite(lang.value, input.value, name)
69 }
70 }
71
72 input.dispatchEvent(enterEvent)
73 }
74
75 function openDB() {
76 return new Promise((resolve, reject) => {
77 const request = window.indexedDB.open(DB_NAME)
78
79 request.onerror = () => reject(request.error)
80
81 request.onsuccess = () => {
82 const database = request.result
83 const missingStores = [HISTORY_STORE, FAVORITES_STORE]
84 .filter(name => !database.objectStoreNames.contains(name))
85
86 if (missingStores.length === 0) {
87 resolve(database)
88 return
89 }
90
91 const version = database.version + 1
92 database.close()
93
94 const upgradeRequest = window.indexedDB.open(DB_NAME, version)
95 upgradeRequest.onupgradeneeded = (event) => {
96 const upgradeDb = event.target.result
97 for (const storeName of missingStores) {
98 if (!upgradeDb.objectStoreNames.contains(storeName)) {
99 upgradeDb.createObjectStore(storeName, {autoIncrement: true})
100 }
101 }
102 }
103 upgradeRequest.onblocked = () => writeError("Please close other tabs with this site open to enable query history.")
104 upgradeRequest.onerror = () => reject(upgradeRequest.error)
105 upgradeRequest.onsuccess = () => resolve(upgradeRequest.result)
106 }
107 })
108 }
109
110 function loadEntries(storeName) {
111 return new Promise((resolve, reject) => {
112 const entries = []
113 const request = db.transaction(storeName).objectStore(storeName).openCursor()
114
115 request.onerror = () => reject(request.error)
116 request.onsuccess = (event) => {
117 const cursor = event.target.result
118 if (cursor) {
119 entries.push({key: cursor.key, ...JSON.parse(cursor.value)})
120 cursor.continue()
121 } else {
122 resolve(entries)
123 }
124 }
125 })
126 }
127
128 function deleteEntry(storeName, key, rerender) {
129 const transaction = db.transaction(storeName, "readwrite")
130 transaction.oncomplete = () => rerender()
131 transaction.objectStore(storeName).delete(key)
132 }
133
134 async function loadEntry(entry) {
135 lang.value = entry.lang
136 input.value = entry.query
137 await runQuery(entry.query, entry.lang)
138 }
139
140 async function recordHistory(queryLang, query) {
141 const entries = await loadEntries(HISTORY_STORE)
142 const last = entries[entries.length - 1]
143 if (last && last.lang === queryLang && last.query === query) {
144 return
145 }
146
147 const transaction = db.transaction(HISTORY_STORE, "readwrite")
148 transaction.objectStore(HISTORY_STORE).add(JSON.stringify({lang: queryLang, query}))
149 transaction.oncomplete = async () => {
150 await trimHistory()
151 await renderHistory()
152 }
153 }
154
155 async function trimHistory() {
156 const entries = await loadEntries(HISTORY_STORE)
157 const excess = entries.length - HISTORY_LIMIT
158 if (excess <= 0) return
159
160 const transaction = db.transaction(HISTORY_STORE, "readwrite")
161 const store = transaction.objectStore(HISTORY_STORE)
162 for (const entry of entries.slice(0, excess)) {
163 store.delete(entry.key)
164 }
165 }
166
167 async function addFavorite(favoriteLang, query, name) {
168 const transaction = db.transaction(FAVORITES_STORE, "readwrite")
169 transaction.objectStore(FAVORITES_STORE).add(JSON.stringify({lang: favoriteLang, query, name}))
170 transaction.oncomplete = () => renderFavorites()
171 }
172
173 async function renderHistory() {
174 const list = getById("historyList")
175 list.replaceChildren()
176
177 const entries = await loadEntries(HISTORY_STORE)
178 for (const entry of entries.reverse()) {
179 list.appendChild(h("span", `[${entry.lang}] ${previewQuery(entry.query)}`))
180 list.appendChild(h("button", {onClick: () => loadEntry(entry)}, "Load"))
181 list.appendChild(h("button", {onClick: () => deleteEntry(HISTORY_STORE, entry.key, renderHistory)}, "Delete"))
182 }
183 }
184
185 async function renderFavorites() {
186 const list = getById("favoritesList")
187 list.replaceChildren()
188
189 const entries = await loadEntries(FAVORITES_STORE)
190 for (const entry of entries) {
191 list.appendChild(h("span", `${entry.name} [${entry.lang}]`))
192 list.appendChild(h("button", {onClick: () => loadEntry(entry)}, "Load"))
193 list.appendChild(h("button", {onClick: () => deleteEntry(FAVORITES_STORE, entry.key, renderFavorites)}, "Delete"))
194 }
195 }
196
197 function previewQuery(query) {
198 const oneLine = query.replace(/\s+/g, " ").trim()
199 return oneLine.length > 60 ? oneLine.slice(0, 60) + "…" : oneLine
200 }
201
202 function getFavoriteName() {
203 return new Promise((resolve) => {
204 const dialog = getById("favoriteNameDialog")
205 const form = getById("favoriteNameForm")
206 const nameInput = getById("favoriteNameInput")
207 const cancelButton = getById("cancelFavoriteNameButton")
208
209 dialog.showModal()
210 nameInput.focus()
211
212 const onCancel = () => {
213 dialog.close("cancelFavoriteName")
214 cancelButton.removeEventListener("click", onCancel)
215 form.removeEventListener("submit", onSubmit)
216 resolve(null)
217 }
218
219 const onSubmit = () => {
220 const value = nameInput.value
221 nameInput.value = ""
222 form.removeEventListener("submit", onSubmit)
223 cancelButton.removeEventListener("click", onCancel)
224 resolve(value || null)
225 }
226
227 cancelButton.addEventListener("click", onCancel)
228 form.addEventListener("submit", onSubmit)
229 })
230 }
231
232 async function runQuery(query, lang) {
233 const results = getById("results")
234
235 try {
236 let result = lang === "jsonql"
237 ? jsonql(query, {personen: dactal.data.personen ?? []})
238 : await dactal.query(query)
239
240 results.replaceChildren(renderValue(result.slice(0, 100)))
241
242 if (result.length > 100) {
243 results.appendChild(h("div", [h("span", `Not showing ${result.length - 100} more results.`)]))
244 }
245 } catch (e) {
246 writeError(e.message)
247 results.replaceChildren()
248 }
249 }
250
251 function renderValue(obj) {
252 if (Array.isArray(obj)) {
253 return h("ol",
254 obj.map((item) => h("li", [renderValue(item)]))
255 )
256 } else if (typeof obj === "object") {
257 return h("ul",
258 Object.entries(obj).map(([name, item]) =>
259 h("li", {class: "dash"},
260 [
261 h("span", {class: "name"}, name),
262 t(" "),
263 renderValue(item)
264 ])))
265 } else if (typeof obj === "boolean") {
266 return h("span", {class: obj ? "json-true" : "json-false"})
267 } else if (typeof obj === "number") {
268 return h("span", {class: "json-number"}, obj.toString())
269 } else if (typeof obj === "string") {
270 return h("span", obj)
271 }
272 }