quar 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
File without changes
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # Quar Studio – Quick UI for Mongoose Models
2
+
3
+ <p align="center">
4
+ <img src="./public/assets/icon.png" alt="logo" width="200px"/>
5
+ </p>
6
+
7
+ Instantly spin up a web-based editor for your Mongoose models.
8
+ Built for developers who loves quick database interaction!
9
+
10
+ ## Install & Run
11
+ ```bash
12
+ npx quar --model <path-to-model-folder> --db <db-name>
13
+ ```
14
+ This will load all Mongoose models from the folder and start a local web UI to Create, view, update, and delete documents.
15
+
16
+ ## What it does
17
+ - Loads your Mongoose models dynamically
18
+ - Connects to your MongoDB database
19
+ - Gives a tabbed UI for each model
20
+ - View nested documents in a tree-style
21
+ - Supports Create, Read, update and delete
22
+
23
+ ## Folder Structure
24
+ Your models folder can contain files like this:
25
+
26
+ ```js
27
+ // ./models/Post.js
28
+ import mongoose from "mongoose"
29
+
30
+ const postSchema = new mongoose.Schema({
31
+ title: String,
32
+ body: String,
33
+ author: {
34
+ name: String,
35
+ age: Number
36
+ }
37
+ });
38
+
39
+ export default mongoose.model("Post", postSchema);
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ 1. Install Quar as dev deps
45
+ ```bash
46
+ npm i quar --save-dev
47
+ ```
48
+
49
+ 2. Execute Command
50
+ ```bash
51
+ npx quar
52
+ ```
53
+
54
+ 3. Set Models Folder
55
+ ```bash
56
+ npx quar --model <folder-path>
57
+ ```
58
+
59
+ 4. Set Database Name
60
+ ```bash
61
+ npx quar --model <folder-path> --db <db-name>
62
+ ```
63
+
64
+ 5. Set Database URI `(Optional)`
65
+ ```bash
66
+ npx quar --model <folder-path> --db <db-name> --uri <db-uri>
67
+ ```
68
+ > ***Note:** By default it will try to connect local mongodb if uri not provided
69
+
70
+ ## Usage Tips
71
+ - Supports nested schemas & subdocuments
72
+ - Clean tabbed navigation per model
73
+ - Auto-refreshes data after updates
74
+ - Keyboard shortcuts support
75
+
76
+ | Command | Performs |
77
+ |---------|----------|
78
+ | <kbd>ctrl + o</kbd> | Toggles Insert Tab |
79
+ | <kbd>ctrl + r</kbd> | Refresh Model |
80
+ | <kbd>ctrl + ></kbd> | Go To Next Page |
81
+ | <kbd>ctrl + <</kbd> | Go To Previous Page |
82
+
83
+
84
+
85
+ ## Dev Notes
86
+
87
+ - Built for development use
88
+ - Doesn’t expose MONGO_URI publicly
89
+ - Uses Mongoose’s model.schema.tree for schema info
90
+ - Modular and extendable
91
+
92
+ ## Contribute
93
+ Pull requests, suggestions, and ideas are welcome!
package/index.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import minimist from 'minimist';
5
+ import app from './server.js';
6
+ import loadModels from './utils/loadModels.js';
7
+ import open from 'open';
8
+ import chalk from 'chalk';
9
+ import mongoose from 'mongoose';
10
+
11
+ const args = minimist(process.argv.slice(2));
12
+
13
+ if (!args.model) {
14
+ console.log(
15
+ chalk.red.bold('[ERROR]') + ' Missing required flag: ' + chalk.yellow('--model')
16
+ );
17
+ console.log('Usage: ' + chalk.cyan('quar --model <model-folder> --db <database-name>'));
18
+ process.exit(1);
19
+ }
20
+
21
+ if (typeof args.model !== 'string') {
22
+ console.log(
23
+ chalk.red.bold('[ERROR]') + ' The value for ' + chalk.yellow('--model') + ' must be a string.'
24
+ );
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!args.db) {
29
+ console.log(
30
+ chalk.red.bold('[ERROR]') + ' Missing required flag: ' + chalk.yellow('--db')
31
+ );
32
+ console.log('Usage: ' + chalk.cyan('quar --model <model-folder> --db <database-name>'));
33
+ process.exit(1);
34
+ }
35
+
36
+ if (typeof args.db !== 'string') {
37
+ console.log(
38
+ chalk.red.bold('[ERROR]') + ' The value for ' + chalk.yellow('--db') + ' must be a string.'
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ const modelDir = path.resolve(process.cwd(), args.model);
44
+ const fullModelPath = path.resolve(modelDir);
45
+
46
+ // 🔃 Load Models and Start Server
47
+ try {
48
+ loadModels(fullModelPath);
49
+ app.locals.modelPath = fullModelPath;
50
+
51
+ (async () => {
52
+
53
+ await mongoose.connect(`${args.uri || "mongodb://localhost:27017/"}${args.db}`)
54
+ app.listen(app.get('port'), () => {
55
+ const url = `http://127.0.0.1:${app.get('port')}`;
56
+ console.log(chalk.green.bold('[SUCCESS]') + ` Server is running on ${chalk.underline(url)}`);
57
+ open(url);
58
+ });
59
+ })();
60
+ } catch (err) {
61
+ console.log(chalk.red.bold('[FATAL ERROR]') + ' Failed to launch the server.');
62
+ console.error(chalk.red(err.stack));
63
+ process.exit(1);
64
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "quar",
3
+ "version": "1.1.0",
4
+ "description": "This will load all Mongoose models from the folder and start a local web UI to Create, view, update, and delete documents.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "quar": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "author": "@IsmailBinMujeeb (IsmailBinMujeeb@gmail.com)",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "chalk": "^5.4.1",
17
+ "express": "^5.1.0",
18
+ "minimist": "^1.2.8",
19
+ "mongoose": "^8.14.1",
20
+ "open": "^10.1.2",
21
+ "zare": "^2.1.1"
22
+ }
23
+ }
Binary file
@@ -0,0 +1,103 @@
1
+ async function toggleInsertTab() {
2
+ const modelName = content.dataset?.modelName;
3
+ if (!modelName) {
4
+ showModal('error', 'Error Occurred!', 'No model selected.');
5
+ return;
6
+ }
7
+
8
+ const insertTab = document.querySelector('.insert-tab');
9
+ insertTab.classList.toggle('active');
10
+
11
+ if (insertTab.innerHTML.trim() !== '') return;
12
+
13
+ try {
14
+ const res = await fetch(`/schema/${modelName}`);
15
+ if (!res.ok) throw new Error();
16
+
17
+ const schema = await res.json();
18
+
19
+ const form = document.createElement('form');
20
+ form.id = 'insert-form';
21
+
22
+ form.addEventListener('submit', async (e) => {
23
+ e.preventDefault();
24
+
25
+ const formData = new FormData(e.target);
26
+ const data = Object.fromEntries(formData.entries());
27
+
28
+ const res = await fetch(`/insert/${modelName}`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify(data)
32
+ });
33
+
34
+ if (!res.ok) {
35
+ const error = await res.json();
36
+ showModal('error', 'Error Occurred!', error.error || 'Something went wrong, please try again.');
37
+ return;
38
+ }
39
+
40
+ loadDocuments();
41
+ });
42
+
43
+ for (const key in schema) {
44
+ const field = schema[key];
45
+
46
+ const label = document.createElement('label');
47
+ label.innerHTML = `${key}:`;
48
+ label.className = 'label';
49
+ label.htmlFor = key;
50
+
51
+ if (field.type === 'ObjectId') {
52
+ const idRes = await fetch(`/id/${field.ref}`);
53
+ if (!idRes.ok) {
54
+ const error = await idRes.json();
55
+ showModal('error', 'Error Occurred!', error.error || 'Something went wrong, please try again.');
56
+ return;
57
+ };
58
+
59
+ const ids = await idRes.json();
60
+ const select = document.createElement('select');
61
+ select.id = key;
62
+ select.name = key;
63
+ select.className = 'input';
64
+
65
+ ids.forEach(id => {
66
+ const option = document.createElement('option');
67
+ option.value = id;
68
+ option.innerText = id;
69
+ select.appendChild(option);
70
+ });
71
+
72
+ form.append(label, select);
73
+ continue;
74
+ }
75
+
76
+ const input = document.createElement('input');
77
+ input.type = field.type || 'text';
78
+ input.placeholder = field.type;
79
+ input.id = key;
80
+ input.name = key;
81
+ input.className = 'input';
82
+ input.required = isRequired(field.require);
83
+ if (field.default !== undefined) input.value = field.default;
84
+
85
+ form.append(label, input);
86
+ }
87
+
88
+ const submitBtn = document.createElement('button');
89
+ submitBtn.type = 'submit';
90
+ submitBtn.innerHTML = 'Submit';
91
+ submitBtn.className = 'btn';
92
+ form.appendChild(submitBtn);
93
+
94
+ insertTab.appendChild(form);
95
+
96
+ } catch (error) {
97
+ showModal('error', 'Error Occurred!', error.message || 'Something went wrong, please try again.');
98
+ }
99
+ }
100
+
101
+ function isRequired(require) {
102
+ return Array.isArray(require) ? require[0] : require;
103
+ }
@@ -0,0 +1,16 @@
1
+ document.addEventListener("keydown", (e) => {
2
+
3
+ if (e.ctrlKey && e.key === 'o') {
4
+ e.preventDefault();
5
+ toggleInsertTab();
6
+ } else if (e.ctrlKey && e.key == "r") {
7
+ e.preventDefault()
8
+ loadDocuments();
9
+ } else if (e.ctrlKey && e.key == ".") {
10
+ e.preventDefault()
11
+ gotoNextPage();
12
+ } else if (e.ctrlKey && e.key == ",") {
13
+ e.preventDefault()
14
+ gotoPreviousPage();
15
+ }
16
+ })
@@ -0,0 +1,327 @@
1
+ const tabsContainer = document.getElementById("tabs");
2
+ const content = document.getElementById("content");
3
+ let currentModelName = "";
4
+ let loadedDocuments = [];
5
+ const openTabs = {};
6
+
7
+ function openModel(modelName) {
8
+
9
+ if (!openTabs[modelName]) {
10
+ // Create a new tab
11
+ const tab = document.createElement("div");
12
+ tab.className = "tab";
13
+ tab.id = "tab-" + modelName;
14
+ tab.draggable = true;
15
+ tab.innerHTML = modelName + '<span class="close" onclick="closeTab(event, \'' + modelName + '\')">×</span>';
16
+ tab.onclick = () => activateTab(modelName);
17
+ tabsContainer.appendChild(tab);
18
+
19
+ tab.addEventListener("dragstart", dragStart);
20
+ tab.addEventListener("dragover", dragOver);
21
+ tab.addEventListener("drop", drop);
22
+ tab.addEventListener("dragend", dragEnd);
23
+
24
+ openTabs[modelName] = tab;
25
+ }
26
+
27
+
28
+ activateTab(modelName);
29
+ }
30
+
31
+ async function activateTab(modelName) {
32
+
33
+ // Prevent if tab is already active
34
+ if (content.dataset?.modelName == modelName) return
35
+
36
+ content.dataset.modelName = modelName;
37
+ document.getElementById("page-value").innerText = 1;
38
+
39
+ const insertTab = document.querySelector('.insert-tab');
40
+ insertTab.classList.remove('active');
41
+ insertTab.innerHTML = "";
42
+ // Remove active class from all tabs
43
+ document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
44
+
45
+ // Set current tab active
46
+ const currentTab = document.getElementById("tab-" + modelName);
47
+ currentTab.classList.add("active");
48
+
49
+ // Update content
50
+ await loadDocuments();
51
+ }
52
+
53
+ function closeTab(event, modelName) {
54
+ event.stopPropagation(); // prevent tab click
55
+ const tab = document.getElementById("tab-" + modelName);
56
+ tab.remove();
57
+ delete openTabs[modelName];
58
+
59
+ // Clear content if that tab was active and check if other tabs are open to be active
60
+ if (tab.classList.contains("active")) {
61
+ content.innerHTML = "";
62
+ const keys = Object.keys(openTabs);
63
+ const modelName = keys[keys.length - 1] || null;
64
+ if (modelName) activateTab(modelName);
65
+ }
66
+ }
67
+
68
+ async function loadDocuments() {
69
+
70
+ content.innerHTML = "";
71
+ const limit = document.getElementById("limit").value;
72
+ const page = document.getElementById("page-value").innerText;
73
+ const modelName = content.dataset?.modelName;
74
+
75
+ if (!modelName) {
76
+ showModal('error', 'Error Occurred!', 'No model selected.');
77
+ return;
78
+ }
79
+
80
+ const response = await fetch(`/models/${modelName}?limit=${limit}&page=${page}`);
81
+ if (response.status !== 200) {
82
+ showModal('error', 'Error Occurred!', 'Something went wrong, please try again.');
83
+ return;
84
+ }
85
+ const data = await response.json();
86
+
87
+ document.getElementById("document-count").innerText = data.count;
88
+ document.getElementById("page-value").dataset.totalPages = data.totalPages;
89
+ document.getElementById(`${modelName}-doc-count`).innerText = data.totalCount;
90
+
91
+ if (Number(page) >= Number(data.totalPages)) document.getElementById("next-page").disabled = true;
92
+ else document.getElementById("next-page").disabled = false;
93
+
94
+ if (Number(page) <= 1) document.getElementById("previous-page").disabled = true;
95
+ else document.getElementById("previous-page").disabled = false;
96
+
97
+ currentModelName = modelName;
98
+ loadedDocuments = data.documents;
99
+ // Render each array element as its own tree
100
+ data.documents.forEach((doc, index) => {
101
+ const wrapper = document.createElement('div');
102
+ wrapper.appendChild(createTree(doc, data.schema, index));
103
+ wrapper.innerHTML += `<div class="document-actions"><button class="updateDocument" onclick="updateDocument(${index})">Update</button><button class="deleteDocument" onclick="deleteDoc('${modelName}', '${doc._id}')">Delete</button></div>`;
104
+ content.innerHTML += wrapper.innerHTML;
105
+ });
106
+ }
107
+
108
+ function gotoNextPage() {
109
+ const page = document.getElementById("page-value");
110
+ page.innerText = Number(page.innerText) + 1;
111
+
112
+ if (Number(page.innerText) > Number(page.dataset.totalPages)) {
113
+ page.innerText = Number(page.dataset.totalPages);
114
+ return;
115
+ }
116
+
117
+ loadDocuments();
118
+ }
119
+
120
+ function gotoPreviousPage() {
121
+ const page = document.getElementById("page-value").innerText;
122
+ document.getElementById("page-value").innerText = Number(page) - 1;
123
+
124
+ if (Number(page) - 1 < 1) {
125
+ document.getElementById("page-value").innerText = 1;
126
+ return;
127
+ }
128
+
129
+ loadDocuments();
130
+ }
131
+
132
+ function createTree(obj, schema, index, level = "root") {
133
+
134
+ const moduleDocument = document.createElement('div');
135
+ moduleDocument.classList.add('document');
136
+ const container = document.createElement('ul');
137
+ container.classList.add('tree');
138
+
139
+ for (let key in obj) {
140
+ const value = obj[key];
141
+ const type = schema[key]?.instance;
142
+ const li = document.createElement('li');
143
+
144
+ if (typeof value === 'object' && value !== null) {
145
+ li.innerHTML = `<span class="caret collapsible">${key} ${type || "Object"}</span>`;
146
+ const child = createTree(value, schema[key]?.options?.type?.paths, index, level + "." + key);
147
+ child.classList.add('nested');
148
+ child.classList.remove('document');
149
+ li.appendChild(child);
150
+ } else {
151
+ li.innerHTML = `<span class="key">${key}</span> : <span class="value ${type}" ${type === "ObjectId" ? "contenteditable='false'" : "contenteditable='true'"} data-level="${level}" data-index="${index}">${value}</span>`;
152
+ }
153
+
154
+ container.appendChild(li);
155
+ }
156
+
157
+ moduleDocument.appendChild(container);
158
+ return moduleDocument;
159
+ }
160
+
161
+ // Handle collapsibles
162
+ document.addEventListener('click', function (e) {
163
+ if (e.target.classList.contains('collapsible')) {
164
+ e.target.classList.toggle("caret-down");
165
+ const nested = e.target.nextElementSibling;
166
+ if (nested) {
167
+ nested.classList.toggle("active");
168
+ }
169
+ }
170
+ });
171
+
172
+ function setNestedValue(obj, path, value) {
173
+ const keys = path.split(".");
174
+ let current = obj;
175
+
176
+ keys.forEach((key, index) => {
177
+ if (index === keys.length - 1) {
178
+ current[key] = value; // Set value at last key
179
+ } else {
180
+ if (!current[key] || typeof current[key] !== "object") {
181
+ current[key] = {}; // Create nested object if it doesn't exist
182
+ }
183
+ current = current[key]; // Go deeper
184
+ }
185
+ });
186
+
187
+ return obj; // Optional: return the updated object
188
+ }
189
+
190
+ async function updateDocument(index) {
191
+
192
+ const confirmUpdate = confirm("Are you sure you want to update this document?");
193
+ if (!confirmUpdate) return;
194
+
195
+ const doc = loadedDocuments[index];
196
+ const id = doc._id;
197
+
198
+ let updatedDoc = {};
199
+ const values = document.querySelectorAll(`[data-index="${index}"]`);
200
+
201
+ values.forEach(el => {
202
+ const key = el.previousElementSibling?.innerText?.trim();
203
+ const level = el.dataset.level;
204
+ if (key) {
205
+ if (level && level !== "root") {
206
+ // Build the full path
207
+ const fullPath = `${level.replace("root.", "")}.${key}`;
208
+ setNestedValue(updatedDoc, fullPath, el.innerText);
209
+ } else {
210
+ updatedDoc[key] = el.innerText;
211
+ }
212
+ }
213
+ });
214
+
215
+ const res = await fetch(`/update/${currentModelName}/${id}`, {
216
+ method: "PUT",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify(updatedDoc),
219
+ });
220
+
221
+ if (res.ok) {
222
+ showModal('info', 'Document Updated', 'Document updated successfully.');
223
+ loadDocuments();
224
+ } else {
225
+ const data = await res.json();
226
+ showModal('error', 'Update Failed', data.error || 'Update failed, please try again.');
227
+ }
228
+ }
229
+
230
+ async function deleteDoc(modelName, id) {
231
+ const confirmDelete = confirm("Are you sure you want to delete this document?");
232
+ if (!confirmDelete) return;
233
+
234
+ const res = await fetch(`/delete/${modelName}/${id}`, {
235
+ method: "DELETE"
236
+ });
237
+
238
+ if (res.ok) {
239
+ showModal('info', 'Document Deleted', 'Document deleted successfully.');
240
+ openModel(modelName);
241
+ const data = await res.json();
242
+ document.getElementById(`${modelName}-doc-count`).innerText = data.count || 0;
243
+ } else {
244
+ showModal('error', 'Delete Failed', await res.json().error || 'Delete failed, please try again.');
245
+ }
246
+ }
247
+
248
+ async function deleteAllDocs() {
249
+ try {
250
+ const confirmDelete = confirm("Are you sure you want to delete all documents?");
251
+ if (!confirmDelete) return;
252
+
253
+ const modelName = content.dataset?.modelName;
254
+ if (!modelName) {
255
+ showModal('error', 'Error Occurred!', 'No model selected.');
256
+ return;
257
+ }
258
+
259
+ const res = await fetch(`/delete-all/${modelName}`, { method: "DELETE" });
260
+ if (res.ok) {
261
+ showModal('info', 'All Documents Deleted', 'All documents deleted successfully.');
262
+ loadDocuments();
263
+ } else {
264
+ showModal('error', 'Delete Failed', await res.json().error || 'Delete failed, please try again.');
265
+ }
266
+ } catch (error) {
267
+ showModal('error', 'Delete Failed', error.message || 'Delete failed, please try again.');
268
+ }
269
+ }
270
+
271
+ function dragStart(e) {
272
+ dragSrc = this;
273
+ e.dataTransfer.effectAllowed = "move";
274
+ this.classList.add("dragging");
275
+ }
276
+
277
+ function dragOver(e) {
278
+ e.preventDefault();
279
+ e.dataTransfer.dropEffect = "move";
280
+ }
281
+
282
+ function drop(e) {
283
+ e.preventDefault();
284
+ if (dragSrc !== this) {
285
+ // Swap positions
286
+ const draggedIndex = Array.from(tabsContainer.children).indexOf(dragSrc);
287
+ const targetIndex = Array.from(tabsContainer.children).indexOf(this);
288
+
289
+ if (draggedIndex < targetIndex) {
290
+ tabsContainer.insertBefore(dragSrc, this.nextSibling);
291
+ } else {
292
+ tabsContainer.insertBefore(dragSrc, this);
293
+ }
294
+ }
295
+ }
296
+
297
+ function dragEnd() {
298
+ this.classList.remove("dragging");
299
+ }
300
+
301
+ async function refreshSideBar() {
302
+ try {
303
+ const modelWrapper = document.querySelector('.model-wrapper');
304
+
305
+ const res = await fetch("/models");
306
+
307
+ if (!res.ok) {
308
+ const error = await res.json();
309
+ showModal('error', 'Error Occurred!', error.error || 'Something went wrong, please try again.');
310
+ return;
311
+ }
312
+
313
+ modelWrapper.innerHTML = "";
314
+ const data = await res.json();
315
+
316
+ data.models.forEach(model => {
317
+ const modelDiv = document.createElement('div');
318
+ modelDiv.classList.add('model');
319
+ modelDiv.onclick = () => openModel(model.name);
320
+ modelDiv.innerHTML = `${model.name} <span id="${model.name}-doc-count">${model.count}</span>`;
321
+ modelWrapper.appendChild(modelDiv);
322
+ });
323
+ } catch (error) {
324
+ showModal('error', 'Error Occurred!', error.message || 'Something went wrong, please try again.');
325
+ }
326
+ }
327
+
@@ -0,0 +1,26 @@
1
+ function showModal(type, title, message) {
2
+ const popup = document.getElementById('popup');
3
+ const box = document.getElementById('popupBox');
4
+ const titleEl = document.getElementById('popupTitle');
5
+ const msgEl = document.getElementById('popupMessage');
6
+
7
+ box.className = 'popup-box';
8
+ if (type === 'warning') box.classList.add('warning');
9
+ else if (type === 'info') box.classList.add('info');
10
+
11
+ titleEl.textContent = title;
12
+ msgEl.textContent = message;
13
+
14
+ popup.style.display = 'flex';
15
+ }
16
+
17
+ function closePopup() {
18
+ document.getElementById('popup').style.display = 'none';
19
+ }
20
+
21
+ window.onclick = function (e) {
22
+ const popup = document.getElementById('popup');
23
+ if (e.target === popup) {
24
+ closePopup();
25
+ }
26
+ }