4bnode 4.1.5 → 4.1.6

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.
Files changed (68) hide show
  1. package/add-api.js +5 -3
  2. package/add-email.js +118 -28
  3. package/add-login.js +35 -16
  4. package/add-mongo-schema.js +13 -5
  5. package/lib/codegen.js +191 -57
  6. package/lib/codegen.test.js +87 -15
  7. package/lib/names.js +1 -1
  8. package/package.json +1 -1
  9. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +54 -9
  10. package/skeleton/.4bnode/assets/js/pages/LogsPage.js +49 -16
  11. package/skeleton/.4bnode/assets/js/pages/MailPage.js +110 -50
  12. package/skeleton/.4bnode/codegen.js +191 -57
  13. package/skeleton/.4bnode/dev-api.js +284 -56
  14. package/skeleton/.env.example +17 -3
  15. package/skeleton/_gitignore +1 -0
  16. package/skeleton/index.js +7 -2
  17. package/skeleton/src/middleware/apiKey.js +14 -1
  18. package/skeleton/src/middleware/errorHandler.js +7 -2
  19. package/app-1/.4bnode/.passkey +0 -1
  20. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  21. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  22. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  23. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  24. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  25. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  26. package/app-1/.4bnode/assets/js/App.js +0 -139
  27. package/app-1/.4bnode/assets/js/Root.js +0 -40
  28. package/app-1/.4bnode/assets/js/components/Modal.js +0 -52
  29. package/app-1/.4bnode/assets/js/components/Sidebar.js +0 -73
  30. package/app-1/.4bnode/assets/js/components/ToastContainer.js +0 -15
  31. package/app-1/.4bnode/assets/js/context.js +0 -7
  32. package/app-1/.4bnode/assets/js/lib.js +0 -113
  33. package/app-1/.4bnode/assets/js/main.js +0 -5
  34. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +0 -268
  35. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +0 -1879
  36. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +0 -435
  37. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +0 -300
  38. package/app-1/.4bnode/assets/js/pages/DocsPage.js +0 -150
  39. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +0 -103
  40. package/app-1/.4bnode/assets/js/pages/LogsPage.js +0 -205
  41. package/app-1/.4bnode/assets/js/pages/MailPage.js +0 -224
  42. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +0 -186
  43. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +0 -1485
  44. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +0 -838
  45. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +0 -74
  46. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +0 -170
  47. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +0 -105
  48. package/app-1/.4bnode/codegen.js +0 -1440
  49. package/app-1/.4bnode/dev-api.js +0 -3747
  50. package/app-1/.4bnode/ui.html +0 -1578
  51. package/app-1/.env.example +0 -11
  52. package/app-1/README.md +0 -47
  53. package/app-1/ecosystem.config.cjs +0 -25
  54. package/app-1/index.js +0 -188
  55. package/app-1/package.json +0 -37
  56. package/app-1/public/index.html +0 -292
  57. package/app-1/src/db.js +0 -13
  58. package/app-1/src/docs.js +0 -84
  59. package/app-1/src/middleware/apiKey.js +0 -36
  60. package/app-1/src/middleware/auth.js +0 -17
  61. package/app-1/src/middleware/errorHandler.js +0 -15
  62. package/app-1/src/middleware/roles.js +0 -14
  63. package/app-1/src/middleware/validate.js +0 -20
  64. package/app-1/src/models/user.js +0 -11
  65. package/app-1/src/openapi.json +0 -80
  66. package/app-1/src/routes/upload-resume.js +0 -21
  67. package/app-1/src/services/mailer.js +0 -52
  68. package/app-1/src/validators/upload-resume.js +0 -8
@@ -1,838 +0,0 @@
1
- import React, { useState, useEffect, useRef } from "react";
2
- import { LuFileCode, LuLock } from "react-icons/lu";
3
- import { html, api, Icons } from "../lib.js";
4
- import { useApp } from "../context.js";
5
-
6
- export function SchemasPage() {
7
- const { data, toast, refresh, setPage, showConfirm } = useApp();
8
- const [showCreate, setShowCreate] = useState(false);
9
- const [editModel, setEditModel] = useState(null);
10
- const [selectedModel, setSelectedModel] = useState(null);
11
-
12
- // Auto-select first model
13
- useEffect(() => {
14
- if (
15
- data.models?.length > 0 &&
16
- !selectedModel &&
17
- !showCreate &&
18
- !editModel
19
- ) {
20
- setSelectedModel(data.models[0].name);
21
- }
22
- }, [data.models]);
23
-
24
- if (!data.mongoConfigured) {
25
- return html`<div className="locked-state">
26
- <div className="locked-icon"><${LuLock} size=${24} /></div>
27
- <h3>Database Required</h3>
28
- <p>
29
- You need to set up a database before creating schemas. Schemas
30
- define the structure of your data.
31
- </p>
32
- <button
33
- className="btn btn-primary"
34
- onClick=${() => setPage("database")}
35
- >
36
- Set Up Database
37
- </button>
38
- </div>`;
39
- }
40
-
41
- const selected = data.models?.find((m) => m.name === selectedModel);
42
- const reqCount =
43
- selected?.fields?.filter((f) => f.required).length || 0;
44
- const uniqCount = selected?.fields?.filter((f) => f.unique).length || 0;
45
- const isEditing = showCreate || !!editModel;
46
-
47
- // Empty state + no create open
48
- if (data.models?.length === 0 && !showCreate) {
49
- return html`<div>
50
- <div className="flex items-center justify-between mb-4">
51
- <div className="card-subtitle">0 models defined</div>
52
- <button
53
- className="btn btn-primary"
54
- onClick=${() => {
55
- setShowCreate(true);
56
- setSelectedModel(null);
57
- setEditModel(null);
58
- }}
59
- >
60
- <span className="sidebar-icon">${Icons.plus}</span> New Schema
61
- </button>
62
- </div>
63
- <div className="empty-state">
64
- <div className="empty-icon"><${LuFileCode} size=${36} /></div>
65
- <h3>No schemas yet</h3>
66
- <p>
67
- Create your first model schema to define your data structure
68
- </p>
69
- <button
70
- className="btn btn-primary"
71
- onClick=${() => setShowCreate(true)}
72
- >
73
- Create Schema
74
- </button>
75
- </div>
76
- </div>`;
77
- }
78
-
79
- return html`<div>
80
- <div className="flex items-center justify-between mb-4">
81
- <div className="card-subtitle">
82
- ${data.models?.length || 0}
83
- model${data.models?.length !== 1 ? "s" : ""} defined
84
- </div>
85
- <button
86
- className=${`btn ${isEditing ? "btn-ghost" : "btn-primary"}`}
87
- onClick=${() => {
88
- if (isEditing) {
89
- setShowCreate(false);
90
- setEditModel(null);
91
- if (data.models?.length)
92
- setSelectedModel(data.models[0].name);
93
- } else {
94
- setShowCreate(true);
95
- setSelectedModel(null);
96
- setEditModel(null);
97
- }
98
- }}
99
- >
100
- ${isEditing
101
- ? "Cancel"
102
- : html`<span className="sidebar-icon">${Icons.plus}</span> New
103
- Schema`}
104
- </button>
105
- </div>
106
-
107
- <div className="routes-layout">
108
- <div className="card" style=${{ padding: 0, overflow: "hidden" }}>
109
- <div
110
- style=${{
111
- padding: "14px 16px 10px",
112
- borderBottom: "1px solid var(--border)",
113
- }}
114
- >
115
- <span
116
- style=${{
117
- fontWeight: 600,
118
- fontSize: "12px",
119
- textTransform: "uppercase",
120
- letterSpacing: "0.5px",
121
- color: "var(--text-muted)",
122
- }}
123
- >Models</span
124
- >
125
- </div>
126
- <div className="routes-list">
127
- ${(data.models || []).map((m) => {
128
- const isActive =
129
- selectedModel === m.name && !showCreate && !editModel;
130
- const isEditActive = editModel?.name === m.name;
131
- return html`<div
132
- key=${m.name}
133
- className=${`route-item ${isActive || isEditActive ? "active" : ""}`}
134
- onClick=${() => {
135
- setShowCreate(false);
136
- setEditModel(null);
137
- setSelectedModel(m.name);
138
- }}
139
- >
140
- <div
141
- style=${{
142
- display: "flex",
143
- flexDirection: "column",
144
- gap: "4px",
145
- flex: 1,
146
- minWidth: 0,
147
- }}
148
- >
149
- <span className="ri-name">${m.name}</span>
150
- <span className="ri-file">src/models/${m.name}.js</span>
151
- </div>
152
- <span className="field-count"
153
- >${m.fields?.length || 0}</span
154
- >
155
- </div>`;
156
- })}
157
- <div
158
- className=${`route-item ${showCreate ? "active" : ""}`}
159
- onClick=${() => {
160
- setShowCreate(true);
161
- setSelectedModel(null);
162
- setEditModel(null);
163
- }}
164
- style=${{
165
- borderTop: data.models?.length
166
- ? "1px solid var(--border)"
167
- : "none",
168
- }}
169
- >
170
- <span
171
- className="sidebar-icon"
172
- style=${{ color: "var(--accent-light)" }}
173
- >${Icons.plus}</span
174
- >
175
- <span
176
- className="ri-name"
177
- style=${{ color: "var(--accent-light)" }}
178
- >New Schema</span
179
- >
180
- </div>
181
- </div>
182
- </div>
183
-
184
- <div className="routes-detail">
185
- ${showCreate &&
186
- html`<${SchemaBuilderPanel}
187
- onDone=${() => {
188
- setShowCreate(false);
189
- refresh();
190
- }}
191
- />`}
192
- ${editModel &&
193
- !showCreate &&
194
- html`<${SchemaBuilderPanel}
195
- model=${editModel}
196
- onDone=${() => {
197
- setEditModel(null);
198
- refresh();
199
- }}
200
- />`}
201
- ${!showCreate &&
202
- !editModel &&
203
- !selected &&
204
- html`<div className="card">
205
- <div className="empty-state" style=${{ padding: "40px" }}>
206
- <p>Select a model to view its fields</p>
207
- </div>
208
- </div>`}
209
- ${!showCreate &&
210
- !editModel &&
211
- selected &&
212
- html`<${SchemaDetailPanel} model=${selected} reqCount=${reqCount} uniqCount=${uniqCount}
213
- onEdit=${() => { setEditModel(selected); setSelectedModel(null); }}
214
- onDelete=${async () => {
215
- const ok = await showConfirm('Are you sure you want to delete the model "' + selected.name + '"? This will remove the schema file.', { danger: true, confirmLabel: 'Delete Model' });
216
- if (!ok) return;
217
- try { await api("/models/" + selected.name, { method: "DELETE" }); toast('Model "' + selected.name + '" deleted', "success"); setSelectedModel(null); refresh(); }
218
- catch (err) { toast(err.message, "error"); }
219
- }}
220
- />`}
221
- </div>
222
- </div>
223
- </div>`;
224
- }
225
-
226
- function SchemaDetailPanel({ model, reqCount, uniqCount, onEdit, onDelete }) {
227
- const thStyle = { padding: '10px 16px', fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', textAlign: 'left', borderBottom: '1px solid var(--border)' };
228
- const tdStyle = { padding: '8px 16px', fontSize: '12px', borderBottom: '1px solid var(--border)' };
229
-
230
- return html`<div>
231
- <div className="card mb-4">
232
- <div className="flex items-center justify-between mb-3">
233
- <div>
234
- <div style=${{ fontWeight: 600, fontSize: '16px', marginBottom: '2px' }}>${model.name}</div>
235
- <div className="text-sm text-muted" style=${{ fontFamily: "'SF Mono', monospace" }}>src/models/${model.name}.js</div>
236
- </div>
237
- <div className="flex gap-2">
238
- <button className="btn btn-ghost btn-sm" onClick=${onEdit}>
239
- <span className="sidebar-icon">${Icons.edit}</span> Edit
240
- </button>
241
- <button className="btn btn-danger btn-sm btn-icon" onClick=${onDelete}>
242
- <span className="sidebar-icon">${Icons.trash}</span>
243
- </button>
244
- </div>
245
- </div>
246
- <div className="flex gap-3">
247
- ${[['Fields', model.fields?.length || 0, null], ['Required', reqCount, reqCount > 0 ? 'var(--success)' : null], ['Unique', uniqCount, uniqCount > 0 ? 'var(--warning)' : null], ['Indexes', (model.fields?.filter(f => f.index).length || 0) + (model.indexes?.length || 0), ((model.fields?.filter(f => f.index).length || 0) + (model.indexes?.length || 0)) > 0 ? 'var(--info)' : null]].map(([label, count, color]) =>
248
- html`<div key=${label} style=${{ padding: '8px 14px', background: 'var(--bg-input)', borderRadius: 'var(--radius)', fontSize: '13px' }}>
249
- <span style=${{ color: 'var(--text-muted)' }}>${label} </span>
250
- <span style=${{ fontWeight: 600, color: color || 'var(--text-secondary)' }}>${count}</span>
251
- </div>`
252
- )}
253
- </div>
254
- </div>
255
-
256
- <div className="card" style=${{ padding: 0, overflow: 'hidden' }}>
257
- <div style=${{ padding: '12px 16px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
258
- <span style=${{ fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)' }}>Schema Fields</span>
259
- </div>
260
- <table style=${{ width: '100%' }}>
261
- <thead><tr>
262
- <th style=${thStyle}>#</th><th style=${thStyle}>Name</th><th style=${thStyle}>Type</th><th style=${thStyle}>Properties</th>
263
- </tr></thead>
264
- <tbody>
265
- ${model.fields?.map((f, i) => html`<tr key=${f.name} style=${{ borderBottom: '1px solid var(--border)' }}>
266
- <td style=${{ ...tdStyle, color: 'var(--text-muted)' }}>${i + 1}</td>
267
- <td style=${{ ...tdStyle, fontWeight: 500, fontSize: '13px', fontFamily: "'SF Mono', monospace" }}>${f.name}</td>
268
- <td style=${tdStyle}><span style=${{ fontSize: '12px', padding: '2px 8px', borderRadius: '4px', background: 'var(--accent-glow)', color: 'var(--accent-light)' }}>${f.type || 'String'}</span></td>
269
- <td style=${tdStyle}><div className="flex gap-1">
270
- ${f.required && html`<span className="badge badge-success" style=${{ fontSize: '10px' }}>required</span>`}
271
- ${f.unique && html`<span className="badge badge-warning" style=${{ fontSize: '10px' }}>unique</span>`}
272
- ${f.index && html`<span className="badge badge-info" style=${{ fontSize: '10px' }}>indexed</span>`}
273
- ${!f.required && !f.unique && !f.index && html`<span style=${{ fontSize: '12px', color: 'var(--text-muted)' }}>—</span>`}
274
- </div></td>
275
- </tr>`)}
276
- </tbody>
277
- </table>
278
- </div>
279
- </div>`;
280
- }
281
-
282
- function SchemaBuilderPanel({ model, onDone }) {
283
- const { data, toast, refresh } = useApp();
284
- const [name, setName] = useState(model?.name || "");
285
- const [fields, setFields] = useState(
286
- model?.fields?.map((f) => ({ ...f })) || [],
287
- );
288
- const [loading, setLoading] = useState(false);
289
- const [saved, setSaved] = useState(false);
290
- const [editIndex, setEditIndex] = useState(null);
291
- const isEdit = !!model;
292
-
293
- // Add/edit field form state
294
- const [fname, setFname] = useState("");
295
- const [ftype, setFtype] = useState("String");
296
- const [freq, setFreq] = useState(false);
297
- const [funiq, setFuniq] = useState(false);
298
- const [fref, setFref] = useState("");
299
- const [findex, setFindex] = useState(false);
300
- const [indexes, setIndexes] = useState(model?.indexes || []);
301
- const [idxFields, setIdxFields] = useState([]);
302
- const [idxType, setIdxType] = useState('compound');
303
- const [idxUnique, setIdxUnique] = useState(false);
304
- const nameRef = useRef(null);
305
- const fieldNameRef = useRef(null);
306
-
307
- const fieldTypes = [
308
- "String",
309
- "Number",
310
- "Boolean",
311
- "Date",
312
- "ObjectId",
313
- "Array",
314
- "Mixed",
315
- "Buffer",
316
- "Map",
317
- ];
318
-
319
- // Get other model names for ObjectId ref
320
- const otherModels = (data.models || [])
321
- .map((m) => m.name)
322
- .filter((n) => n !== name);
323
-
324
- useEffect(() => {
325
- if (!isEdit && nameRef.current) nameRef.current.focus();
326
- else if (isEdit && fieldNameRef.current) fieldNameRef.current.focus();
327
- }, []);
328
-
329
- function resetFieldForm() {
330
- setFname("");
331
- setFtype("String");
332
- setFreq(false);
333
- setFuniq(false);
334
- setFref("");
335
- setFindex(false);
336
- setEditIndex(null);
337
- }
338
-
339
- function addOrUpdateField() {
340
- if (!fname.trim()) return;
341
- const fieldData = {
342
- name: fname.trim(),
343
- type: ftype,
344
- required: freq,
345
- unique: funiq,
346
- };
347
- if (ftype === "ObjectId" && fref) fieldData.ref = fref;
348
- if (findex) fieldData.index = true;
349
-
350
- if (editIndex !== null) {
351
- setFields((prev) =>
352
- prev.map((f, i) => (i === editIndex ? fieldData : f)),
353
- );
354
- setEditIndex(null);
355
- } else {
356
- setFields((prev) => [...prev, fieldData]);
357
- }
358
- resetFieldForm();
359
- setSaved(false);
360
- setTimeout(() => fieldNameRef.current?.focus(), 50);
361
- }
362
-
363
- function startEdit(index) {
364
- const f = fields[index];
365
- setFname(f.name);
366
- setFtype(f.type || "String");
367
- setFreq(!!f.required);
368
- setFuniq(!!f.unique);
369
- setFref(f.ref || "");
370
- setFindex(!!f.index);
371
- setEditIndex(index);
372
- setTimeout(() => fieldNameRef.current?.focus(), 50);
373
- }
374
-
375
- function removeField(index) {
376
- setFields((prev) => prev.filter((_, i) => i !== index));
377
- if (editIndex === index) resetFieldForm();
378
- setSaved(false);
379
- }
380
-
381
- async function handleSave() {
382
- if (!name.trim()) {
383
- toast("Model name is required", "error");
384
- return;
385
- }
386
- if (fields.length === 0) {
387
- toast("Add at least one field", "error");
388
- return;
389
- }
390
- setLoading(true);
391
- try {
392
- if (isEdit) {
393
- await api("/models/" + model.name, {
394
- method: "PUT",
395
- body: { fields, indexes },
396
- });
397
- toast('Model "' + model.name + '" updated', "success");
398
- } else {
399
- await api("/models", {
400
- method: "POST",
401
- body: { name: name.trim(), fields, indexes },
402
- });
403
- toast('Model "' + name.trim() + '" created', "success");
404
- }
405
- setSaved(true);
406
- refresh();
407
- onDone();
408
- } catch (err) {
409
- toast(err.message, "error");
410
- }
411
- setLoading(false);
412
- }
413
-
414
- const typeColors = {
415
- String: "var(--accent-light)",
416
- Number: "var(--success)",
417
- Boolean: "var(--warning)",
418
- Date: "#a78bfa",
419
- ObjectId: "#f472b6",
420
- Array: "#22d3ee",
421
- Mixed: "var(--text-secondary)",
422
- Buffer: "#fb923c",
423
- Map: "#34d399",
424
- };
425
-
426
- return html`<div>
427
- <div className="card mb-4">
428
- <div
429
- className="flex items-center justify-between"
430
- style=${{ marginBottom: "16px" }}
431
- >
432
- <div style=${{ fontSize: "15px", fontWeight: 600 }}>
433
- ${isEdit ? "Edit Schema: " + model.name : "Create New Schema"}
434
- </div>
435
- <button
436
- className="btn btn-primary btn-sm"
437
- onClick=${handleSave}
438
- disabled=${loading || fields.length === 0 || !name.trim()}
439
- >
440
- ${loading ? html`<span className="spinner" />` : null}
441
- ${saved ? "\u2713 Saved" : isEdit ? "Update" : "Save Schema"}
442
- </button>
443
- </div>
444
-
445
- ${!isEdit &&
446
- html`<div className="form-group">
447
- <label className="form-label">Model Name</label>
448
- <input
449
- ref=${nameRef}
450
- className="form-input"
451
- value=${name}
452
- onInput=${(e) => setName(e.target.value)}
453
- placeholder="e.g. user, product, post"
454
- style=${{ fontSize: "14px", padding: "11px 14px" }}
455
- />
456
- </div>`}
457
-
458
- <div
459
- style=${{
460
- fontSize: "11px",
461
- fontWeight: 600,
462
- textTransform: "uppercase",
463
- letterSpacing: "0.5px",
464
- color:
465
- editIndex !== null ? "var(--warning)" : "var(--text-muted)",
466
- marginBottom: "10px",
467
- marginTop: isEdit ? 0 : "8px",
468
- }}
469
- >
470
- ${editIndex !== null ? "Edit Field" : "Add Field"}
471
- </div>
472
-
473
- <div
474
- className="flex items-center gap-3 mb-3"
475
- style=${{ flexWrap: "wrap" }}
476
- >
477
- <input
478
- ref=${fieldNameRef}
479
- className="form-input"
480
- value=${fname}
481
- onInput=${(e) => setFname(e.target.value)}
482
- onKeyDown=${(e) => e.key === "Enter" && addOrUpdateField()}
483
- placeholder="Field name"
484
- style=${{
485
- fontSize: "13px",
486
- flex: "1 1 120px",
487
- minWidth: "100px",
488
- }}
489
- />
490
- <select
491
- className="form-select"
492
- value=${ftype}
493
- onChange=${(e) => {
494
- setFtype(e.target.value);
495
- if (e.target.value !== "ObjectId") setFref("");
496
- }}
497
- style=${{ fontSize: "13px", width: "110px", flex: "0 0 110px" }}
498
- >
499
- ${fieldTypes.map(
500
- (t) => html`<option key=${t} value=${t}>${t}</option>`,
501
- )}
502
- </select>
503
- ${ftype === "ObjectId" &&
504
- html`<select
505
- className="form-select"
506
- value=${fref}
507
- onChange=${(e) => setFref(e.target.value)}
508
- style=${{ fontSize: "13px", width: "110px", flex: "0 0 110px" }}
509
- >
510
- <option value="">Ref...</option>
511
- ${otherModels.map(
512
- (m) => html`<option key=${m} value=${m}>${m}</option>`,
513
- )}
514
- </select>`}
515
- <div style=${{ display: 'flex', alignItems: 'center', gap: '16px', padding: '0 4px' }}>
516
- <label
517
- className="flex items-center gap-2"
518
- style=${{
519
- cursor: "pointer",
520
- fontSize: "12px",
521
- whiteSpace: "nowrap",
522
- color: freq ? "var(--success)" : "var(--text-muted)",
523
- }}
524
- >
525
- <div className="form-toggle">
526
- <input
527
- type="checkbox"
528
- checked=${freq}
529
- onChange=${(e) => setFreq(e.target.checked)}
530
- />
531
- <span className="slider" />
532
- </div>
533
- Required
534
- </label>
535
- <label
536
- className="flex items-center gap-2"
537
- style=${{
538
- cursor: "pointer",
539
- fontSize: "12px",
540
- whiteSpace: "nowrap",
541
- color: funiq ? "var(--warning)" : "var(--text-muted)",
542
- }}
543
- >
544
- <div className="form-toggle">
545
- <input
546
- type="checkbox"
547
- checked=${funiq}
548
- onChange=${(e) => setFuniq(e.target.checked)}
549
- />
550
- <span className="slider" />
551
- </div>
552
- Unique
553
- </label>
554
- <label
555
- className="flex items-center gap-2"
556
- style=${{
557
- cursor: "pointer",
558
- fontSize: "12px",
559
- whiteSpace: "nowrap",
560
- color: findex ? "var(--info)" : "var(--text-muted)",
561
- }}
562
- >
563
- <div className="form-toggle">
564
- <input
565
- type="checkbox"
566
- checked=${findex}
567
- onChange=${(e) => setFindex(e.target.checked)}
568
- />
569
- <span className="slider" />
570
- </div>
571
- Index
572
- </label>
573
- </div>
574
- ${editIndex !== null &&
575
- html`<button
576
- className="btn btn-ghost btn-sm"
577
- onClick=${resetFieldForm}
578
- style=${{ padding: "5px 8px" }}
579
- >
580
- Cancel
581
- </button>`}
582
- <button
583
- className="btn ${editIndex !== null
584
- ? "btn-warning"
585
- : "btn-primary"} btn-sm"
586
- onClick=${addOrUpdateField}
587
- disabled=${!fname.trim()}
588
- style=${{ padding: "5px 10px", whiteSpace: "nowrap" }}
589
- >
590
- ${editIndex !== null
591
- ? "Update"
592
- : html`<span className="sidebar-icon">${Icons.plus}</span>
593
- Add`}
594
- </button>
595
- </div>
596
- </div>
597
-
598
- ${fields.length > 0 &&
599
- html`<div
600
- className="card mb-4"
601
- style=${{ padding: 0, overflow: "hidden" }}
602
- >
603
- <div
604
- style=${{
605
- padding: "12px 16px",
606
- borderBottom: "1px solid var(--border)",
607
- display: "flex",
608
- alignItems: "center",
609
- justifyContent: "space-between",
610
- }}
611
- >
612
- <span
613
- style=${{
614
- fontSize: "12px",
615
- fontWeight: 600,
616
- textTransform: "uppercase",
617
- letterSpacing: "0.5px",
618
- color: "var(--text-muted)",
619
- }}
620
- >Fields</span
621
- >
622
- <span className="field-count">${fields.length}</span>
623
- </div>
624
- <div style=${{ maxHeight: "340px", overflowY: "auto" }}>
625
- ${fields.map(
626
- (f, i) =>
627
- html`<div
628
- key=${i}
629
- className="flex items-center gap-3"
630
- style=${{
631
- padding: "8px 16px",
632
- borderBottom: "1px solid rgba(255,255,255,0.03)",
633
- background:
634
- editIndex === i ? "var(--warning-glow)" : "transparent",
635
- transition: "background var(--transition)",
636
- }}
637
- onMouseEnter=${(e) => {
638
- if (editIndex !== i)
639
- e.currentTarget.style.background =
640
- "rgba(255,255,255,0.02)";
641
- }}
642
- onMouseLeave=${(e) => {
643
- if (editIndex !== i)
644
- e.currentTarget.style.background = "transparent";
645
- }}
646
- >
647
- <span
648
- style=${{
649
- fontSize: "11px",
650
- color: "var(--text-muted)",
651
- width: "20px",
652
- fontVariantNumeric: "tabular-nums",
653
- }}
654
- >${i + 1}</span
655
- >
656
- <span
657
- style=${{
658
- fontWeight: 500,
659
- fontSize: "13px",
660
- flex: 1,
661
- fontFamily: "'SF Mono', 'Fira Code', monospace",
662
- }}
663
- >${f.name}</span
664
- >
665
- <span
666
- style=${{
667
- fontSize: "11px",
668
- padding: "2px 8px",
669
- borderRadius: "4px",
670
- background: "rgba(255,255,255,0.04)",
671
- color: typeColors[f.type] || "var(--text-secondary)",
672
- }}
673
- >${f.type}${f.ref ? " \u2192 " + f.ref : ""}</span
674
- >
675
- ${f.required &&
676
- html`<span
677
- style=${{
678
- fontSize: "9px",
679
- padding: "2px 6px",
680
- borderRadius: "4px",
681
- background: "var(--success-glow)",
682
- color: "var(--success)",
683
- fontWeight: 600,
684
- }}
685
- >REQ</span
686
- >`}
687
- ${f.unique &&
688
- html`<span
689
- style=${{
690
- fontSize: "9px",
691
- padding: "2px 6px",
692
- borderRadius: "4px",
693
- background: "var(--warning-glow)",
694
- color: "var(--warning)",
695
- fontWeight: 600,
696
- }}
697
- >UNQ</span
698
- >`}
699
- ${f.index &&
700
- html`<span
701
- style=${{
702
- fontSize: "9px",
703
- padding: "2px 6px",
704
- borderRadius: "4px",
705
- background: "rgba(59,130,246,0.15)",
706
- color: "var(--info)",
707
- fontWeight: 600,
708
- }}
709
- >IDX</span
710
- >`}
711
- <button
712
- className="btn btn-ghost btn-sm btn-icon"
713
- onClick=${() => startEdit(i)}
714
- style=${{
715
- color: "var(--accent-light)",
716
- opacity: 0.6,
717
- padding: "4px",
718
- }}
719
- >
720
- <span className="sidebar-icon">${Icons.edit}</span>
721
- </button>
722
- <button
723
- className="btn btn-ghost btn-sm btn-icon"
724
- onClick=${() => removeField(i)}
725
- style=${{
726
- color: "var(--error)",
727
- opacity: 0.5,
728
- padding: "4px",
729
- }}
730
- >
731
- <span className="sidebar-icon">${Icons.trash}</span>
732
- </button>
733
- </div>`,
734
- )}
735
- </div>
736
- </div>`}
737
- ${fields.length === 0 &&
738
- html`<div
739
- className="card mb-4"
740
- style=${{
741
- background: "var(--bg)",
742
- padding: "30px",
743
- textAlign: "center",
744
- }}
745
- >
746
- <div style=${{ fontSize: "12px", color: "var(--text-muted)" }}>
747
- Add fields above to define your schema
748
- </div>
749
- </div>`}
750
-
751
- <div className="card mb-4" style=${{ padding: '16px' }}>
752
- <div style=${{ fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', marginBottom: '12px' }}>
753
- Compound Indexes
754
- </div>
755
-
756
- <div className="flex items-center gap-3 mb-3" style=${{ flexWrap: 'wrap' }}>
757
- <select className="form-select" value=${idxType}
758
- onChange=${(e) => setIdxType(e.target.value)}
759
- style=${{ fontSize: '13px', width: '120px' }}>
760
- <option value="compound">Compound</option>
761
- <option value="text">Text</option>
762
- </select>
763
-
764
- ${idxType === 'compound' && html`<label className="flex items-center gap-2" style=${{ cursor: 'pointer', fontSize: '12px', color: idxUnique ? 'var(--warning)' : 'var(--text-muted)' }}>
765
- <div className="form-toggle">
766
- <input type="checkbox" checked=${idxUnique} onChange=${(e) => setIdxUnique(e.target.checked)} />
767
- <span className="slider" />
768
- </div>
769
- Unique
770
- </label>`}
771
-
772
- <button className="btn btn-primary btn-sm"
773
- disabled=${idxFields.length < (idxType === 'text' ? 1 : 2)}
774
- onClick=${() => {
775
- const newIdx = {
776
- fields: idxFields.map(f => ({ field: f.field, direction: idxType === 'text' ? 'text' : f.direction })),
777
- unique: idxType === 'compound' ? idxUnique : false,
778
- type: idxType,
779
- };
780
- setIndexes(prev => [...prev, newIdx]);
781
- setIdxFields([]);
782
- setIdxUnique(false);
783
- setSaved(false);
784
- }}>
785
- Add Index
786
- </button>
787
- </div>
788
-
789
- <div className="flex gap-2 mb-3" style=${{ flexWrap: 'wrap' }}>
790
- ${fields.map(f => {
791
- const selected = idxFields.find(i => i.field === f.name);
792
- return html`<button key=${f.name}
793
- onClick=${() => {
794
- if (selected) {
795
- if (idxType !== 'text') {
796
- if (selected.direction === 1) {
797
- setIdxFields(prev => prev.map(i => i.field === f.name ? { ...i, direction: -1 } : i));
798
- } else {
799
- setIdxFields(prev => prev.filter(i => i.field !== f.name));
800
- }
801
- } else {
802
- setIdxFields(prev => prev.filter(i => i.field !== f.name));
803
- }
804
- } else {
805
- setIdxFields(prev => [...prev, { field: f.name, direction: 1 }]);
806
- }
807
- }}
808
- style=${{ padding: '4px 10px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--border)', cursor: 'pointer', fontFamily: 'monospace',
809
- background: selected ? 'var(--accent-glow)' : 'transparent',
810
- color: selected ? 'var(--accent-light)' : 'var(--text-muted)' }}>
811
- ${f.name}${selected && idxType !== 'text' ? (selected.direction === 1 ? ' ASC' : ' DESC') : ''}
812
- </button>`;
813
- })}
814
- </div>
815
- ${idxFields.length > 0 && html`<div style=${{ fontSize: '11px', color: 'var(--text-muted)', marginBottom: '8px' }}>
816
- ${idxType === 'text' ? 'Click to select/deselect fields' : 'Click once = ASC, twice = DESC, third = remove'}
817
- </div>`}
818
-
819
- ${indexes.length > 0 && html`<div style=${{ marginTop: '12px' }}>
820
- ${indexes.map((idx, i) => html`<div key=${i} className="flex items-center gap-2" style=${{ padding: '6px 12px', background: 'var(--bg-input)', borderRadius: '6px', marginBottom: '4px', fontSize: '12px' }}>
821
- <span style=${{ fontSize: '9px', padding: '2px 6px', borderRadius: '4px', fontWeight: 600,
822
- background: idx.type === 'text' ? 'rgba(59,130,246,0.15)' : 'rgba(124,58,237,0.15)',
823
- color: idx.type === 'text' ? 'var(--info)' : 'var(--accent-light)' }}>
824
- ${idx.type === 'text' ? 'TEXT' : 'COMPOUND'}
825
- </span>
826
- <span style=${{ fontFamily: 'monospace', flex: 1 }}>
827
- { ${idx.fields.map(f => f.field + ': ' + (f.direction === 'text' ? "'text'" : f.direction)).join(', ')} }
828
- </span>
829
- ${idx.unique && html`<span style=${{ fontSize: '9px', padding: '2px 6px', borderRadius: '4px', background: 'var(--warning-glow)', color: 'var(--warning)', fontWeight: 600 }}>UNIQUE</span>`}
830
- <button className="btn btn-ghost btn-sm btn-icon" onClick=${() => { setIndexes(prev => prev.filter((_, j) => j !== i)); setSaved(false); }}
831
- style=${{ color: 'var(--error)', opacity: 0.5, padding: '4px' }}>
832
- ${Icons.trash}
833
- </button>
834
- </div>`)}
835
- </div>`}
836
- </div>
837
- </div>`;
838
- }