admindb 1.2.1 → 2.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.
Files changed (208) hide show
  1. package/README.md +262 -329
  2. package/dist/app.d.ts +25 -4
  3. package/dist/app.js +79 -15
  4. package/dist/app.js.map +1 -1
  5. package/dist/auth/config.d.ts +19 -0
  6. package/dist/auth/config.js +147 -0
  7. package/dist/auth/config.js.map +1 -0
  8. package/dist/auth/crypto.d.ts +27 -0
  9. package/dist/auth/crypto.js +133 -0
  10. package/dist/auth/crypto.js.map +1 -0
  11. package/dist/auth/index.d.ts +5 -0
  12. package/dist/auth/index.js +22 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/middleware.d.ts +14 -0
  15. package/dist/auth/middleware.js +100 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/routes.d.ts +7 -0
  18. package/dist/auth/routes.js +78 -0
  19. package/dist/auth/routes.js.map +1 -0
  20. package/dist/auth/types.d.ts +35 -0
  21. package/dist/auth/types.js +11 -0
  22. package/dist/auth/types.js.map +1 -0
  23. package/dist/cli/args.d.ts +45 -0
  24. package/dist/cli/args.js +328 -0
  25. package/dist/cli/args.js.map +1 -0
  26. package/dist/cli/config.d.ts +56 -0
  27. package/dist/cli/config.js +133 -0
  28. package/dist/cli/config.js.map +1 -0
  29. package/dist/cli/index.d.ts +4 -0
  30. package/dist/cli/index.js +21 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/runner.d.ts +1 -0
  33. package/dist/cli/runner.js +222 -0
  34. package/dist/cli/runner.js.map +1 -0
  35. package/dist/cli.js +2 -127
  36. package/dist/cli.js.map +1 -1
  37. package/dist/data/datasets.d.ts +17 -0
  38. package/dist/data/datasets.js +133 -0
  39. package/dist/data/datasets.js.map +1 -0
  40. package/dist/data/detector.d.ts +30 -0
  41. package/dist/data/detector.js +472 -0
  42. package/dist/data/detector.js.map +1 -0
  43. package/dist/data/engine.d.ts +10 -0
  44. package/dist/data/engine.js +211 -0
  45. package/dist/data/engine.js.map +1 -0
  46. package/dist/data/index.d.ts +5 -0
  47. package/dist/data/index.js +22 -0
  48. package/dist/data/index.js.map +1 -0
  49. package/dist/data/strategies.d.ts +30 -0
  50. package/dist/data/strategies.js +343 -0
  51. package/dist/data/strategies.js.map +1 -0
  52. package/dist/data/types.d.ts +73 -0
  53. package/dist/data/types.js +8 -0
  54. package/dist/data/types.js.map +1 -0
  55. package/dist/db/database.d.ts +14 -146
  56. package/dist/db/database.js +208 -461
  57. package/dist/db/database.js.map +1 -1
  58. package/dist/db/export.d.ts +2 -2
  59. package/dist/db/export.js +7 -2
  60. package/dist/db/export.js.map +1 -1
  61. package/dist/db/filters.d.ts +22 -0
  62. package/dist/db/filters.js +158 -0
  63. package/dist/db/filters.js.map +1 -0
  64. package/dist/db/index.d.ts +8 -0
  65. package/dist/db/index.js +25 -0
  66. package/dist/db/index.js.map +1 -0
  67. package/dist/db/introspection.d.ts +6 -0
  68. package/dist/db/introspection.js +172 -0
  69. package/dist/db/introspection.js.map +1 -0
  70. package/dist/db/manager.d.ts +32 -19
  71. package/dist/db/manager.js +203 -44
  72. package/dist/db/manager.js.map +1 -1
  73. package/dist/db/migrations.d.ts +5 -0
  74. package/dist/db/migrations.js +110 -0
  75. package/dist/db/migrations.js.map +1 -0
  76. package/dist/db/postgres.d.ts +85 -0
  77. package/dist/db/postgres.js +745 -0
  78. package/dist/db/postgres.js.map +1 -0
  79. package/dist/db/types.d.ts +206 -0
  80. package/dist/db/types.js +7 -0
  81. package/dist/db/types.js.map +1 -0
  82. package/dist/index.d.ts +9 -2
  83. package/dist/index.js +46 -4
  84. package/dist/index.js.map +1 -1
  85. package/dist/public/css/app.css +1 -1
  86. package/dist/public/css/input.css +74 -11
  87. package/dist/public/js/app.js +142 -0
  88. package/dist/public/js/browse.js +591 -104
  89. package/dist/public/js/databases.js +105 -13
  90. package/dist/public/js/designer.js +55 -2
  91. package/dist/public/js/forms.js +610 -28
  92. package/dist/public/js/icons.js +51 -0
  93. package/dist/public/js/inspector.js +638 -0
  94. package/dist/public/js/query.js +93 -3
  95. package/dist/public/js/schema.js +185 -52
  96. package/dist/public/js/seed.js +865 -152
  97. package/dist/routes/api/helpers.d.ts +42 -0
  98. package/dist/routes/api/helpers.js +133 -0
  99. package/dist/routes/api/helpers.js.map +1 -0
  100. package/dist/routes/api/import-export.d.ts +3 -0
  101. package/dist/routes/api/import-export.js +85 -0
  102. package/dist/routes/api/import-export.js.map +1 -0
  103. package/dist/routes/api/index.d.ts +4 -0
  104. package/dist/routes/api/index.js +37 -0
  105. package/dist/routes/api/index.js.map +1 -0
  106. package/dist/routes/api/query.d.ts +3 -0
  107. package/dist/routes/api/query.js +76 -0
  108. package/dist/routes/api/query.js.map +1 -0
  109. package/dist/routes/api/rows.d.ts +3 -0
  110. package/dist/routes/api/rows.js +373 -0
  111. package/dist/routes/api/rows.js.map +1 -0
  112. package/dist/routes/api/seed.d.ts +3 -0
  113. package/dist/routes/api/seed.js +82 -0
  114. package/dist/routes/api/seed.js.map +1 -0
  115. package/dist/routes/api/tables.d.ts +3 -0
  116. package/dist/routes/api/tables.js +160 -0
  117. package/dist/routes/api/tables.js.map +1 -0
  118. package/dist/routes/databases.d.ts +8 -11
  119. package/dist/routes/databases.js +100 -66
  120. package/dist/routes/databases.js.map +1 -1
  121. package/dist/routes/index.d.ts +3 -0
  122. package/dist/routes/index.js +20 -0
  123. package/dist/routes/index.js.map +1 -0
  124. package/dist/routes/pages.d.ts +3 -3
  125. package/dist/routes/pages.js +157 -64
  126. package/dist/routes/pages.js.map +1 -1
  127. package/dist/serverless.d.ts +40 -0
  128. package/dist/serverless.js +210 -0
  129. package/dist/serverless.js.map +1 -0
  130. package/dist/sql/generator.d.ts +7 -3
  131. package/dist/sql/generator.js +144 -7
  132. package/dist/sql/generator.js.map +1 -1
  133. package/dist/sql/index.d.ts +2 -0
  134. package/dist/sql/index.js +19 -0
  135. package/dist/sql/index.js.map +1 -0
  136. package/dist/types/api.d.ts +412 -0
  137. package/dist/types/api.js +9 -0
  138. package/dist/types/api.js.map +1 -0
  139. package/dist/utils/colors.d.ts +40 -0
  140. package/dist/utils/colors.js +64 -0
  141. package/dist/utils/colors.js.map +1 -0
  142. package/dist/{util.d.ts → utils/common.d.ts} +10 -6
  143. package/dist/{util.js → utils/common.js} +80 -13
  144. package/dist/utils/common.js.map +1 -0
  145. package/dist/utils/csv.js.map +1 -0
  146. package/dist/utils/datatype.d.ts +49 -0
  147. package/dist/utils/datatype.js +222 -0
  148. package/dist/utils/datatype.js.map +1 -0
  149. package/dist/utils/icons.d.ts +11 -0
  150. package/dist/utils/icons.js +72 -0
  151. package/dist/utils/icons.js.map +1 -0
  152. package/dist/utils/index.d.ts +4 -0
  153. package/dist/utils/index.js +21 -0
  154. package/dist/utils/index.js.map +1 -0
  155. package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
  156. package/dist/utils/logger.js +40 -0
  157. package/dist/utils/logger.js.map +1 -0
  158. package/dist/views/layouts/main.hbs +40 -11
  159. package/dist/views/pages/databases.hbs +182 -74
  160. package/dist/views/pages/designer.hbs +29 -11
  161. package/dist/views/pages/error.hbs +3 -3
  162. package/dist/views/pages/form.hbs +5 -3
  163. package/dist/views/pages/home.hbs +22 -13
  164. package/dist/views/pages/login.hbs +111 -0
  165. package/dist/views/pages/query.hbs +8 -6
  166. package/dist/views/pages/schema.hbs +41 -13
  167. package/dist/views/pages/seed.hbs +200 -54
  168. package/dist/views/pages/table.hbs +152 -72
  169. package/dist/views/partials/icon.hbs +2 -0
  170. package/dist/views/partials/navbar.hbs +69 -25
  171. package/dist/views/partials/sidebar.hbs +108 -94
  172. package/docs/API.md +144 -0
  173. package/docs/EXAMPLES.md +576 -0
  174. package/docs/SECURITY.md +265 -0
  175. package/package.json +15 -4
  176. package/dist/args.d.ts +0 -28
  177. package/dist/args.js +0 -187
  178. package/dist/args.js.map +0 -1
  179. package/dist/config.d.ts +0 -17
  180. package/dist/config.js +0 -23
  181. package/dist/config.js.map +0 -1
  182. package/dist/csv.js.map +0 -1
  183. package/dist/data/generator.d.ts +0 -86
  184. package/dist/data/generator.js +0 -1031
  185. package/dist/data/generator.js.map +0 -1
  186. package/dist/logger.js +0 -25
  187. package/dist/logger.js.map +0 -1
  188. package/dist/routes/api.d.ts +0 -9
  189. package/dist/routes/api.js +0 -848
  190. package/dist/routes/api.js.map +0 -1
  191. package/dist/test/classifier.test.d.ts +0 -1
  192. package/dist/test/classifier.test.js +0 -44
  193. package/dist/test/classifier.test.js.map +0 -1
  194. package/dist/test/csv.test.d.ts +0 -1
  195. package/dist/test/csv.test.js +0 -64
  196. package/dist/test/csv.test.js.map +0 -1
  197. package/dist/test/database.test.d.ts +0 -1
  198. package/dist/test/database.test.js +0 -402
  199. package/dist/test/database.test.js.map +0 -1
  200. package/dist/test/generator.test.d.ts +0 -1
  201. package/dist/test/generator.test.js +0 -132
  202. package/dist/test/generator.test.js.map +0 -1
  203. package/dist/test/manager.test.d.ts +0 -1
  204. package/dist/test/manager.test.js +0 -146
  205. package/dist/test/manager.test.js.map +0 -1
  206. package/dist/util.js.map +0 -1
  207. /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
  208. /package/dist/{csv.js → utils/csv.js} +0 -0
@@ -24,28 +24,64 @@
24
24
  return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
25
25
  }
26
26
 
27
- function toLocalInput(v) {
28
- const m = String(v).match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})/);
29
- if (m) return m[1] + 'T' + m[2] + ':' + m[3];
30
- return v;
27
+ function toLocalInput(v, type) {
28
+ if (v == null || v === '') return '';
29
+ const s = String(v);
30
+ const t = (type || '').toUpperCase();
31
+
32
+ // Date only
33
+ if (t === 'DATE') {
34
+ const dm = s.match(/^(\d{4}-\d{2}-\d{2})/);
35
+ return dm ? dm[1] : s;
36
+ }
37
+
38
+ // Time only
39
+ if (t === 'TIME' || t === 'TIMETZ' || t.startsWith('TIME ') || t.startsWith('TIME(')) {
40
+ const tm = s.match(/(\d{2}:\d{2}(?::\d{2})?)/);
41
+ return tm ? tm[1] : s;
42
+ }
43
+
44
+ // Datetime / Timestamp
45
+ const m = s.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/);
46
+ if (m) return m[1] + 'T' + m[2] + ':' + m[3] + (m[4] ? ':' + m[4] : '');
47
+ return s;
31
48
  }
32
49
 
33
- function toLocalNow() {
50
+ function toLocalNow(dateOnly, timeOnly) {
34
51
  const d = new Date();
35
52
  const p = (n) => String(n).padStart(2, '0');
36
- return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes());
53
+ if (dateOnly) {
54
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
55
+ }
56
+ if (timeOnly) {
57
+ return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
58
+ }
59
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
60
+ }
61
+
62
+ function generateUuidV4() {
63
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
64
+ return crypto.randomUUID();
65
+ }
66
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
67
+ const r = (Math.random() * 16) | 0;
68
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
69
+ return v.toString(16);
70
+ });
37
71
  }
38
72
 
39
73
  /**
40
74
  * Convert a column's raw default expression (dflt_value) into a form-friendly
41
- * value. Handles literals we can read safely; anything complex returns null
42
- * (the DB applies the real default when the field is left empty).
75
+ * value. Handles literals we can read safely; anything complex returns null.
43
76
  */
44
77
  function defaultToValue(col, dflt) {
45
78
  if (dflt === null || dflt === undefined) return null;
46
- const s = String(dflt).trim();
79
+ let s = String(dflt).trim();
47
80
  if (s === '' || /^null$/i.test(s)) return null;
48
81
 
82
+ // Strip PostgreSQL type casts, e.g. 'active'::character varying, 0::bigint, true
83
+ s = s.replace(/::[a-zA-Z0-9_\s"\[\]]+$/i, '').trim();
84
+
49
85
  const type = (col.type || '').toUpperCase();
50
86
 
51
87
  // Single-quoted string literal, e.g. 'N/A', 'user'
@@ -58,15 +94,23 @@
58
94
  // Boolean literal
59
95
  if (/^(true|false)$/i.test(s)) return /^true$/i.test(s);
60
96
 
61
- // BOOLEAN columns stored as 0/1
62
- if (type === 'BOOLEAN') {
97
+ // UUID generators
98
+ if (type === 'UUID' && /gen_random_uuid|uuid_generate/i.test(s)) {
99
+ return generateUuidV4();
100
+ }
101
+
102
+ // BOOLEAN columns stored as 0/1 or true/false
103
+ if (type === 'BOOLEAN' || type === 'BOOL') {
63
104
  if (s === '1' || /^true$/i.test(s)) return 1;
64
105
  if (s === '0' || /^false$/i.test(s)) return 0;
65
106
  }
66
107
 
67
- // current-timestamp style defaults → "now" for date/datetime columns
68
- if ((type === 'DATE' || type === 'DATETIME') && /current_timestamp|now\s*\(|datetime\s*\(|date\s*\(/i.test(s)) {
69
- return toLocalNow();
108
+ // current-timestamp / now() style defaults
109
+ if ((type === 'DATE' || type === 'DATETIME' || type.includes('TIMESTAMP')) && /current_timestamp|now\s*\(|datetime\s*\(|date\s*\(|clock_timestamp|statement_timestamp|transaction_timestamp/i.test(s)) {
110
+ return toLocalNow(type === 'DATE', false);
111
+ }
112
+ if ((type === 'TIME' || type === 'TIMETZ') && /current_time|now\s*\(|time\s*\(/i.test(s)) {
113
+ return toLocalNow(false, true);
70
114
  }
71
115
 
72
116
  return null;
@@ -83,6 +127,422 @@
83
127
  return v === 1 || v === true || v === '1' || v === 'true';
84
128
  }
85
129
 
130
+ // ---- Widgets --------------------------------------------------------------
131
+
132
+ function createCalendarWidget(colName, colType, iv, isPk) {
133
+ const type = (colType || '').toUpperCase();
134
+ const isDateOnly = type === 'DATE';
135
+ const isTimeOnly = type === 'TIME' || type === 'TIMETZ' || type.startsWith('TIME ') || type.startsWith('TIME(');
136
+ const isTimestamp = type === 'DATETIME' || type.includes('TIMESTAMP');
137
+
138
+ const container = document.createElement('div');
139
+ container.className = 'space-y-1';
140
+
141
+ const inputRow = document.createElement('div');
142
+ inputRow.className = 'flex items-center gap-1.5';
143
+
144
+ const input = document.createElement('input');
145
+ input.type = isDateOnly ? 'date' : (isTimeOnly ? 'time' : 'datetime-local');
146
+ if (isTimeOnly || isTimestamp) input.step = '1';
147
+ input.className = 'field-input flex-1 font-mono text-[13px]';
148
+ input.dataset.column = colName;
149
+ if (iv != null) input.value = toLocalInput(iv, type);
150
+
151
+ const formatLabel = document.createElement('div');
152
+ formatLabel.className = 'text-[11px] text-base-content/50 font-mono transition-all';
153
+ function updateLabel() {
154
+ if (!input.value) {
155
+ formatLabel.textContent = '(empty / NULL)';
156
+ return;
157
+ }
158
+ try {
159
+ if (isDateOnly) {
160
+ const parts = input.value.split('-');
161
+ const dt = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
162
+ formatLabel.textContent = '📅 ' + dt.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
163
+ } else if (isTimeOnly) {
164
+ formatLabel.textContent = '⏰ ' + input.value;
165
+ } else {
166
+ const dt = new Date(input.value);
167
+ if (!isNaN(dt.getTime())) {
168
+ formatLabel.textContent = '📅 ' + dt.toLocaleString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' });
169
+ } else {
170
+ formatLabel.textContent = input.value;
171
+ }
172
+ }
173
+ } catch (e) {
174
+ formatLabel.textContent = input.value;
175
+ }
176
+ }
177
+ input.addEventListener('input', updateLabel);
178
+ input.addEventListener('change', updateLabel);
179
+ updateLabel();
180
+
181
+ const dd = document.createElement('div');
182
+ dd.className = 'dropdown dropdown-end';
183
+
184
+ const ddBtn = document.createElement('button');
185
+ ddBtn.type = 'button';
186
+ ddBtn.tabIndex = 0;
187
+ ddBtn.className = 'btn btn-ghost btn-sm btn-square text-primary hover:bg-primary/10 transition';
188
+ ddBtn.title = 'Open calendar selector & presets';
189
+ ddBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>';
190
+
191
+ const ddMenu = document.createElement('ul');
192
+ ddMenu.tabIndex = 0;
193
+ ddMenu.className = 'dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-xl w-56 border border-base-300 text-xs space-y-0.5';
194
+
195
+ function addPreset(label, icon, onClick) {
196
+ const li = document.createElement('li');
197
+ const btn = document.createElement('button');
198
+ btn.type = 'button';
199
+ btn.className = 'flex items-center gap-2 py-1.5 font-medium';
200
+ btn.innerHTML = '<span>' + icon + '</span><span>' + label + '</span>';
201
+ btn.addEventListener('click', () => {
202
+ onClick();
203
+ updateLabel();
204
+ if (document.activeElement && typeof document.activeElement.blur === 'function') {
205
+ document.activeElement.blur();
206
+ }
207
+ });
208
+ li.appendChild(btn);
209
+ ddMenu.appendChild(li);
210
+ }
211
+
212
+ addPreset('Open Picker', '📅', () => {
213
+ if (typeof input.showPicker === 'function') {
214
+ try { input.showPicker(); } catch (e) { input.focus(); }
215
+ } else {
216
+ input.focus();
217
+ }
218
+ });
219
+
220
+ if (isTimeOnly) {
221
+ addPreset('Now', '⚡', () => { input.value = toLocalNow(false, true); });
222
+ addPreset('Start of Hour', '⏱️', () => {
223
+ const d = new Date();
224
+ const p = (n) => String(n).padStart(2, '0');
225
+ input.value = p(d.getHours()) + ':00:00';
226
+ });
227
+ } else if (isDateOnly) {
228
+ addPreset('Today', '⚡', () => { input.value = toLocalNow(true, false); });
229
+ addPreset('Yesterday', '⏪', () => {
230
+ const d = new Date();
231
+ d.setDate(d.getDate() - 1);
232
+ const p = (n) => String(n).padStart(2, '0');
233
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
234
+ });
235
+ addPreset('Tomorrow', '⏩', () => {
236
+ const d = new Date();
237
+ d.setDate(d.getDate() + 1);
238
+ const p = (n) => String(n).padStart(2, '0');
239
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
240
+ });
241
+ addPreset('+7 Days (1 Week)', '🗓️', () => {
242
+ const d = new Date();
243
+ d.setDate(d.getDate() + 7);
244
+ const p = (n) => String(n).padStart(2, '0');
245
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
246
+ });
247
+ addPreset('+30 Days (1 Month)', '📆', () => {
248
+ const d = new Date();
249
+ d.setDate(d.getDate() + 30);
250
+ const p = (n) => String(n).padStart(2, '0');
251
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
252
+ });
253
+ } else {
254
+ addPreset('Now', '⚡', () => { input.value = toLocalNow(false, false); });
255
+ addPreset('Today (Start of Day)', '🌅', () => {
256
+ input.value = toLocalNow(true, false) + 'T00:00:00';
257
+ });
258
+ addPreset('Today (End of Day)', '🌃', () => {
259
+ input.value = toLocalNow(true, false) + 'T23:59:59';
260
+ });
261
+ addPreset('Yesterday', '⏪', () => {
262
+ const d = new Date();
263
+ d.setDate(d.getDate() - 1);
264
+ const p = (n) => String(n).padStart(2, '0');
265
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
266
+ });
267
+ addPreset('Tomorrow', '⏩', () => {
268
+ const d = new Date();
269
+ d.setDate(d.getDate() + 1);
270
+ const p = (n) => String(n).padStart(2, '0');
271
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
272
+ });
273
+ addPreset('+7 Days (1 Week)', '🗓️', () => {
274
+ const d = new Date();
275
+ d.setDate(d.getDate() + 7);
276
+ const p = (n) => String(n).padStart(2, '0');
277
+ input.value = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
278
+ });
279
+ }
280
+
281
+ const divider = document.createElement('li');
282
+ divider.className = 'border-t border-base-200 my-1';
283
+ ddMenu.appendChild(divider);
284
+
285
+ addPreset('Clear / Set NULL', '🧹', () => { input.value = ''; });
286
+
287
+ dd.appendChild(ddBtn);
288
+ dd.appendChild(ddMenu);
289
+
290
+ inputRow.appendChild(input);
291
+ inputRow.appendChild(dd);
292
+ container.appendChild(inputRow);
293
+ container.appendChild(formatLabel);
294
+
295
+ if (isPk && cfg.mode === 'edit') input.disabled = true;
296
+ return container;
297
+ }
298
+
299
+ function createArrayWidget(colName, colType, iv, isPk) {
300
+ const container = document.createElement('div');
301
+ container.className = 'space-y-2';
302
+
303
+ const hiddenInput = document.createElement('input');
304
+ hiddenInput.type = 'hidden';
305
+ hiddenInput.dataset.column = colName;
306
+
307
+ let items = [];
308
+ if (Array.isArray(iv)) {
309
+ items = iv.map(String);
310
+ } else if (typeof iv === 'string' && iv.trim().startsWith('{') && iv.trim().endsWith('}')) {
311
+ const inner = iv.trim().slice(1, -1);
312
+ items = inner ? inner.split(',').map((s) => s.trim().replace(/^"(.*)"$/, '$1')) : [];
313
+ } else if (typeof iv === 'string' && iv.trim().startsWith('[') && iv.trim().endsWith(']')) {
314
+ try {
315
+ const parsed = JSON.parse(iv);
316
+ if (Array.isArray(parsed)) items = parsed.map(String);
317
+ } catch (e) {
318
+ items = [];
319
+ }
320
+ } else if (iv != null && String(iv).trim() !== '') {
321
+ items = [String(iv)];
322
+ }
323
+
324
+ function sync() {
325
+ hiddenInput.value = items.length ? '{' + items.map((x) => (x.includes(',') || x.includes('"') ? '"' + x.replace(/"/g, '\\"') + '"' : x)).join(',') + '}' : '';
326
+ renderChips();
327
+ }
328
+
329
+ const chipsContainer = document.createElement('div');
330
+ chipsContainer.className = 'flex flex-wrap gap-1.5 min-h-[32px] p-1.5 rounded-lg border border-base-300 bg-base-100';
331
+
332
+ function renderChips() {
333
+ chipsContainer.innerHTML = '';
334
+ if (!items.length) {
335
+ chipsContainer.innerHTML = '<span class="text-xs text-base-content/40 italic self-center px-1.5">No array items (empty / NULL)</span>';
336
+ return;
337
+ }
338
+ items.forEach((item, idx) => {
339
+ const chip = document.createElement('span');
340
+ chip.className = 'inline-flex items-center gap-1 rounded bg-primary/10 px-2 py-1 text-xs font-mono font-medium text-primary';
341
+ chip.innerHTML = '<span>' + escapeHtml(item) + '</span><button type="button" class="text-primary hover:text-error ml-1 font-bold">&times;</button>';
342
+ chip.querySelector('button').addEventListener('click', () => {
343
+ items.splice(idx, 1);
344
+ sync();
345
+ });
346
+ chipsContainer.appendChild(chip);
347
+ });
348
+ }
349
+
350
+ const addRow = document.createElement('div');
351
+ addRow.className = 'flex gap-1.5';
352
+ const textInput = document.createElement('input');
353
+ textInput.type = 'text';
354
+ textInput.className = 'field-input flex-1 font-mono text-xs';
355
+ textInput.placeholder = 'Type item and press Enter or click Add…';
356
+ const addBtn = document.createElement('button');
357
+ addBtn.type = 'button';
358
+ addBtn.className = 'btn btn-ghost btn-xs text-primary font-medium';
359
+ addBtn.textContent = '+ Add Item';
360
+
361
+ function addItem() {
362
+ const val = textInput.value.trim();
363
+ if (val) {
364
+ if (val.includes(',')) {
365
+ val.split(',').map((x) => x.trim()).filter(Boolean).forEach((x) => items.push(x));
366
+ } else {
367
+ items.push(val);
368
+ }
369
+ textInput.value = '';
370
+ sync();
371
+ }
372
+ }
373
+
374
+ addBtn.addEventListener('click', addItem);
375
+ textInput.addEventListener('keydown', (e) => {
376
+ if (e.key === 'Enter') {
377
+ e.preventDefault();
378
+ addItem();
379
+ }
380
+ });
381
+
382
+ addRow.appendChild(textInput);
383
+ addRow.appendChild(addBtn);
384
+
385
+ container.appendChild(hiddenInput);
386
+ container.appendChild(chipsContainer);
387
+ container.appendChild(addRow);
388
+ sync();
389
+
390
+ if (isPk && cfg.mode === 'edit') textInput.disabled = true;
391
+ return container;
392
+ }
393
+
394
+ function createUuidWidget(colName, iv, isPk) {
395
+ const uuidWrap = document.createElement('div');
396
+ uuidWrap.className = 'space-y-1';
397
+
398
+ const inputRow = document.createElement('div');
399
+ inputRow.className = 'flex items-center gap-1.5';
400
+
401
+ const control = document.createElement('input');
402
+ control.type = 'text';
403
+ control.className = 'field-input font-mono text-xs flex-1';
404
+ control.placeholder = '00000000-0000-0000-0000-000000000000';
405
+ control.dataset.column = colName;
406
+ if (iv != null) control.value = String(iv);
407
+
408
+ const validBadge = document.createElement('span');
409
+ validBadge.className = 'text-[10px] font-mono transition-all text-base-content/40';
410
+
411
+ function checkUuid() {
412
+ const v = control.value.trim();
413
+ if (!v) {
414
+ validBadge.textContent = '';
415
+ return;
416
+ }
417
+ const isOk = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v);
418
+ if (isOk) {
419
+ validBadge.className = 'text-[10px] font-mono text-success font-semibold';
420
+ validBadge.textContent = '✓ Valid UUID v4';
421
+ } else {
422
+ validBadge.className = 'text-[10px] font-mono text-warning';
423
+ validBadge.textContent = '⚠ Custom format';
424
+ }
425
+ }
426
+ control.addEventListener('input', checkUuid);
427
+ checkUuid();
428
+
429
+ const genBtn = document.createElement('button');
430
+ genBtn.type = 'button';
431
+ genBtn.className = 'btn btn-ghost btn-xs gap-1 text-primary text-xs shrink-0';
432
+ genBtn.innerHTML = '✨ Gen UUID';
433
+ genBtn.addEventListener('click', () => {
434
+ control.value = generateUuidV4();
435
+ checkUuid();
436
+ });
437
+
438
+ inputRow.appendChild(control);
439
+ inputRow.appendChild(genBtn);
440
+ uuidWrap.appendChild(inputRow);
441
+ uuidWrap.appendChild(validBadge);
442
+
443
+ if (isPk && cfg.mode === 'edit') control.disabled = true;
444
+ return uuidWrap;
445
+ }
446
+
447
+ function createJsonWidget(colName, colType, iv, isPk) {
448
+ const jsonContainer = document.createElement('div');
449
+ jsonContainer.className = 'space-y-1.5';
450
+
451
+ const control = document.createElement('textarea');
452
+ control.rows = 4;
453
+ control.className = 'field-input font-mono text-[13px] w-full';
454
+ control.placeholder = '{\n "key": "value"\n}';
455
+ control.dataset.column = colName;
456
+ if (iv != null) control.value = typeof iv === 'object' ? JSON.stringify(iv, null, 2) : String(iv);
457
+
458
+ const btnRow = document.createElement('div');
459
+ btnRow.className = 'flex items-center justify-between text-xs';
460
+
461
+ const statusBadge = document.createElement('span');
462
+ statusBadge.className = 'font-mono text-[11px] text-base-content/40';
463
+
464
+ function validateJson() {
465
+ const v = control.value.trim();
466
+ if (!v) {
467
+ statusBadge.textContent = '(empty / NULL)';
468
+ statusBadge.className = 'font-mono text-[11px] text-base-content/40';
469
+ return;
470
+ }
471
+ try {
472
+ JSON.parse(v);
473
+ statusBadge.textContent = '✓ Valid JSON';
474
+ statusBadge.className = 'font-mono text-[11px] text-success font-semibold';
475
+ } catch (e) {
476
+ statusBadge.textContent = '✗ Invalid JSON syntax';
477
+ statusBadge.className = 'font-mono text-[11px] text-error font-medium';
478
+ }
479
+ }
480
+ control.addEventListener('input', validateJson);
481
+ validateJson();
482
+
483
+ const actions = document.createElement('div');
484
+ actions.className = 'flex items-center gap-1';
485
+
486
+ const formatBtn = document.createElement('button');
487
+ formatBtn.type = 'button';
488
+ formatBtn.className = 'btn btn-ghost btn-xs text-xs';
489
+ formatBtn.textContent = 'Beautify';
490
+ formatBtn.addEventListener('click', () => {
491
+ try {
492
+ const parsed = JSON.parse(control.value);
493
+ control.value = JSON.stringify(parsed, null, 2);
494
+ validateJson();
495
+ } catch (e) { UI.showError('Invalid JSON: ' + e.message); }
496
+ });
497
+
498
+ const minifyBtn = document.createElement('button');
499
+ minifyBtn.type = 'button';
500
+ minifyBtn.className = 'btn btn-ghost btn-xs text-xs';
501
+ minifyBtn.textContent = 'Minify';
502
+ minifyBtn.addEventListener('click', () => {
503
+ try {
504
+ const parsed = JSON.parse(control.value);
505
+ control.value = JSON.stringify(parsed);
506
+ validateJson();
507
+ } catch (e) { UI.showError('Invalid JSON: ' + e.message); }
508
+ });
509
+
510
+ const inspectBtn = document.createElement('button');
511
+ inspectBtn.type = 'button';
512
+ inspectBtn.className = 'btn btn-ghost btn-xs gap-1 text-primary text-xs font-semibold';
513
+ inspectBtn.innerHTML = '🧩 Inspector';
514
+ inspectBtn.addEventListener('click', () => {
515
+ if (window.Inspector && window.Inspector.open) {
516
+ window.Inspector.open({
517
+ table: cfg.table,
518
+ pk: cfg.pk || '',
519
+ col: colName,
520
+ colType: colType,
521
+ value: control.value,
522
+ isNull: !control.value,
523
+ isJson: true,
524
+ readonly: false,
525
+ onSave: (newVal) => {
526
+ control.value = newVal;
527
+ validateJson();
528
+ },
529
+ });
530
+ }
531
+ });
532
+
533
+ actions.appendChild(formatBtn);
534
+ actions.appendChild(minifyBtn);
535
+ actions.appendChild(inspectBtn);
536
+
537
+ btnRow.appendChild(statusBadge);
538
+ btnRow.appendChild(actions);
539
+
540
+ jsonContainer.appendChild(control);
541
+ jsonContainer.appendChild(btnRow);
542
+ if (isPk && cfg.mode === 'edit') control.disabled = true;
543
+ return jsonContainer;
544
+ }
545
+
86
546
  function renderField(col) {
87
547
  const isPk = info.primaryKey.indexOf(col.name) !== -1;
88
548
  const wrapper = document.createElement('div');
@@ -100,24 +560,33 @@
100
560
 
101
561
  const fk = info.foreignKeys.find((f) => f.from === col.name);
102
562
  const type = (col.type || '').toUpperCase();
103
- const isBool = type === 'BOOLEAN';
104
- const isNum = type.startsWith('INTEGER') || type === 'REAL';
105
- const isBlob = type === 'BLOB';
106
- const isDate = type === 'DATE' || type === 'DATETIME';
563
+ const isBool = type === 'BOOLEAN' || type === 'BOOL';
564
+ const isNum = type.startsWith('INT') || type === 'REAL' || type === 'NUMERIC' || type.startsWith('DECIMAL') || type === 'FLOAT' || type === 'DOUBLE' || type.startsWith('BIGINT') || type.startsWith('SMALLINT') || type.startsWith('SERIAL') || type === 'MONEY';
565
+ const isBlob = type === 'BLOB' || type === 'BYTEA' || type === 'BINARY';
566
+ const isDate = type === 'DATE';
567
+ const isTime = type === 'TIME' || type === 'TIMETZ' || type.startsWith('TIME ') || type.startsWith('TIME(');
568
+ const isTimestamp = type === 'DATETIME' || type.includes('TIMESTAMP');
569
+ const isUuid = type === 'UUID';
570
+ const isJson = type.includes('JSON') || col.name.toLowerCase().includes('json');
571
+ const isArray = type.endsWith('[]') || type.startsWith('_') || type.includes('ARRAY');
572
+ const isInet = type === 'INET' || type === 'CIDR' || type === 'MACADDR' || type === 'MACADDR8';
573
+ const isInterval = type === 'INTERVAL';
574
+ const isGeometry = type === 'POINT' || type === 'GEOMETRY' || type === 'BOX' || type === 'CIRCLE' || type === 'POLYGON';
575
+ const isBit = type === 'BIT' || type === 'VARBIT' || type.startsWith('BIT(') || type.startsWith('VARBIT(');
107
576
  const iv = initialValue(col);
108
577
 
109
578
  let control;
110
579
  if (isBool) {
111
580
  const boolWrap = document.createElement('label');
112
- boolWrap.className = 'flex cursor-pointer items-center gap-2';
581
+ boolWrap.className = 'flex cursor-pointer items-center gap-2 pt-1';
113
582
  control = document.createElement('input');
114
583
  control.type = 'checkbox';
115
584
  control.className = 'field-check';
116
585
  control.dataset.column = col.name;
117
586
  if (isTruthy(iv)) control.checked = true;
118
587
  const span = document.createElement('span');
119
- span.className = 'text-sm text-base-content/60';
120
- span.textContent = 'Enabled';
588
+ span.className = 'text-sm text-base-content/70';
589
+ span.textContent = 'True / Enabled';
121
590
  boolWrap.appendChild(control);
122
591
  boolWrap.appendChild(span);
123
592
  wrapper.appendChild(boolWrap);
@@ -138,24 +607,137 @@
138
607
  if (iv != null && String(iv) === String(o.value)) opt.selected = true;
139
608
  control.appendChild(opt);
140
609
  });
141
- } else if (isDate) {
610
+ } else if (isDate || isTime || isTimestamp) {
611
+ const calWidget = createCalendarWidget(col.name, col.type, iv, isPk);
612
+ wrapper.appendChild(calWidget);
613
+ return wrapper;
614
+ } else if (isArray) {
615
+ const arrWidget = createArrayWidget(col.name, col.type, iv, isPk);
616
+ wrapper.appendChild(arrWidget);
617
+ return wrapper;
618
+ } else if (isUuid) {
619
+ const uuidWidget = createUuidWidget(col.name, iv, isPk);
620
+ wrapper.appendChild(uuidWidget);
621
+ return wrapper;
622
+ } else if (isJson || (iv && typeof iv === 'object')) {
623
+ const jsonWidget = createJsonWidget(col.name, col.type, iv, isPk);
624
+ wrapper.appendChild(jsonWidget);
625
+ return wrapper;
626
+ } else if (isInterval) {
627
+ const intWrap = document.createElement('div');
628
+ intWrap.className = 'space-y-1.5';
142
629
  control = document.createElement('input');
143
- control.type = 'datetime-local';
144
- control.className = 'field-input';
630
+ control.type = 'text';
631
+ control.className = 'field-input font-mono text-xs';
632
+ control.placeholder = "e.g. '1 day', '02:30:00', '1 month 2 days'";
633
+ control.dataset.column = col.name;
634
+ if (iv != null) control.value = String(iv);
635
+ const presets = document.createElement('div');
636
+ presets.className = 'flex flex-wrap gap-1';
637
+ ['1 day', '1 week', '1 month', '1 year', '1 hour', '30 mins'].forEach((preset) => {
638
+ const btn = document.createElement('button');
639
+ btn.type = 'button';
640
+ btn.className = 'btn btn-ghost btn-xs text-[11px] font-mono';
641
+ btn.textContent = preset;
642
+ btn.addEventListener('click', () => { control.value = preset; });
643
+ presets.appendChild(btn);
644
+ });
645
+ intWrap.appendChild(control);
646
+ intWrap.appendChild(presets);
647
+ if (isPk && cfg.mode === 'edit') control.disabled = true;
648
+ wrapper.appendChild(intWrap);
649
+ return wrapper;
650
+ } else if (isGeometry) {
651
+ control = document.createElement('input');
652
+ control.type = 'text';
653
+ control.className = 'field-input font-mono text-xs';
654
+ control.placeholder = "(x, y) e.g. '(12.345, 67.890)'";
145
655
  control.dataset.column = col.name;
146
- if (iv != null) control.value = toLocalInput(String(iv));
656
+ if (iv != null) control.value = String(iv);
657
+ } else if (isBit) {
658
+ control = document.createElement('input');
659
+ control.type = 'text';
660
+ control.className = 'field-input font-mono text-xs';
661
+ control.placeholder = "Binary e.g. '101010'";
662
+ control.dataset.column = col.name;
663
+ if (iv != null) control.value = String(iv);
664
+ } else if (isInet) {
665
+ control = document.createElement('input');
666
+ control.type = 'text';
667
+ control.className = 'field-input font-mono text-xs';
668
+ control.placeholder = type === 'MACADDR' ? '08:00:2b:01:02:03' : '192.168.1.1 or 2001:db8::1';
669
+ control.dataset.column = col.name;
670
+ if (iv != null) control.value = String(iv);
147
671
  } else if (isNum) {
148
672
  control = document.createElement('input');
149
673
  control.type = 'number';
150
- control.step = type === 'REAL' ? 'any' : '1';
151
- control.className = 'field-input';
674
+ const isInteger = type.startsWith('INTEGER') || type === 'INT' || type.startsWith('BIGINT') || type.startsWith('SMALLINT') || type.startsWith('SERIAL');
675
+ control.step = isInteger ? '1' : 'any';
676
+ control.className = 'field-input font-mono';
152
677
  control.dataset.column = col.name;
678
+ if (type.startsWith('SERIAL') && cfg.mode === 'insert' && (iv == null || iv === '')) {
679
+ control.placeholder = '(auto-increment serial)';
680
+ }
153
681
  if (iv != null) control.value = String(iv);
682
+ } else if (isBlob) {
683
+ const blobContainer = document.createElement('div');
684
+ blobContainer.className = 'space-y-2';
685
+
686
+ control = document.createElement('input');
687
+ control.type = 'text';
688
+ control.className = 'field-input font-mono text-xs';
689
+ control.placeholder = type === 'BYTEA' ? '\\x… or 0x… hex' : '0x… hex or plain text';
690
+ control.dataset.column = col.name;
691
+ if (iv != null) control.value = String(iv);
692
+
693
+ const fileRow = document.createElement('div');
694
+ fileRow.className = 'flex flex-wrap items-center gap-2';
695
+
696
+ const fileInput = document.createElement('input');
697
+ fileInput.type = 'file';
698
+ fileInput.className = 'file-input file-input-bordered file-input-xs w-full max-w-xs';
699
+
700
+ const previewThumb = document.createElement('img');
701
+ previewThumb.className = 'h-7 max-w-12 rounded object-cover border border-base-300 bg-base-200 hidden';
702
+
703
+ fileInput.addEventListener('change', (e) => {
704
+ const file = e.target.files && e.target.files[0];
705
+ if (!file) return;
706
+ const reader = new FileReader();
707
+ reader.onload = () => {
708
+ const arrBuffer = reader.result;
709
+ const bytes = new Uint8Array(arrBuffer);
710
+ let hex = type === 'BYTEA' ? '\\x' : '0x';
711
+ for (let i = 0; i < bytes.length; i++) {
712
+ hex += ('00' + bytes[i].toString(16)).slice(-2);
713
+ }
714
+ control.value = hex;
715
+
716
+ if (file.type.startsWith('image/')) {
717
+ const dataUrlReader = new FileReader();
718
+ dataUrlReader.onload = () => {
719
+ previewThumb.src = dataUrlReader.result;
720
+ previewThumb.classList.remove('hidden');
721
+ };
722
+ dataUrlReader.readAsDataURL(file);
723
+ } else {
724
+ previewThumb.classList.add('hidden');
725
+ }
726
+ };
727
+ reader.readAsArrayBuffer(file);
728
+ });
729
+
730
+ fileRow.appendChild(fileInput);
731
+ fileRow.appendChild(previewThumb);
732
+ blobContainer.appendChild(control);
733
+ blobContainer.appendChild(fileRow);
734
+ if (isPk && cfg.mode === 'edit') control.disabled = true;
735
+ wrapper.appendChild(blobContainer);
736
+ return wrapper;
154
737
  } else {
155
738
  control = document.createElement('input');
156
739
  control.type = 'text';
157
740
  control.className = 'field-input';
158
- control.placeholder = isBlob ? '0x… hex or text' : '';
159
741
  control.dataset.column = col.name;
160
742
  if (iv != null) control.value = String(iv);
161
743
  }