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
@@ -1,13 +1,8 @@
1
- /* "Seed data" page.
1
+ /* "Seed data" page — enhanced interactive data generator.
2
2
  *
3
- * Lets you generate N realistic rows for a table. Each column gets a strategy
4
- * (auto-detected from its name/type/foreign key, but overridable): skip,
5
- * set NULL, fixed value, random from list, or any of the built-in fake-data
6
- * generators (first/last name, email, phone, UUID, random int/date/…), plus
7
- * "random from referenced table" for foreign-key columns.
8
- *
9
- * "Preview SQL" generates the rows and shows the INSERT statements without
10
- * executing them; "Insert rows" writes them inside a single transaction.
3
+ * Lets you generate realistic fake rows for a table with 20+ strategies,
4
+ * live preview samples, sparse data simulation, sequential numbering,
5
+ * pattern templates, dual Table/SQL previews, CSV/JSON export, and presets.
11
6
  */
12
7
  (function () {
13
8
  'use strict';
@@ -19,180 +14,569 @@
19
14
  const t = encodeURIComponent(cfg.table);
20
15
  const columns = cfg.columns || [];
21
16
 
17
+ // DOM Elements
22
18
  const countInput = document.getElementById('seed-count');
19
+ const searchInput = document.getElementById('seed-search');
20
+ const truncateCheckbox = document.getElementById('seed-truncate');
23
21
  const columnsBox = document.getElementById('seed-columns');
22
+ const emptySearchBox = document.getElementById('seed-empty-search');
23
+ const colCountBadge = document.getElementById('seed-col-count-badge');
24
24
  const feedback = document.getElementById('seed-feedback');
25
- const sqlBox = document.getElementById('seed-sql');
25
+ const previewCard = document.getElementById('seed-preview-card');
26
+ const previewSummaryBadge = document.getElementById('preview-summary-badge');
27
+ const previewThead = document.getElementById('seed-preview-thead');
28
+ const previewTbody = document.getElementById('seed-preview-tbody');
29
+ const tableViewContainer = document.getElementById('seed-table-view-container');
30
+ const sqlViewContainer = document.getElementById('seed-sql-view-container');
26
31
  const sqlCode = document.getElementById('seed-sql-code');
27
- const copyBtn = document.getElementById('seed-copy');
32
+ const tabTableView = document.getElementById('tab-table-view');
33
+ const tabSqlView = document.getElementById('tab-sql-view');
34
+
35
+ // Action buttons
28
36
  const insertBtn = document.getElementById('seed-insert');
29
37
  const previewBtn = document.getElementById('seed-preview');
30
38
  const resetBtn = document.getElementById('seed-reset');
39
+ const refreshSamplesBtn = document.getElementById('seed-refresh-samples');
31
40
  const applyAllBtn = document.getElementById('seed-apply-all');
41
+ const copySqlBtn = document.getElementById('seed-copy-sql');
42
+ const downloadSqlBtn = document.getElementById('seed-download-sql');
43
+ const exportCsvBtn = document.getElementById('seed-export-csv');
44
+ const exportJsonBtn = document.getElementById('seed-export-json');
45
+ const exportConfigBtn = document.getElementById('seed-export-config');
46
+ const importConfigBtn = document.getElementById('seed-import-config');
47
+
48
+ // Modals
49
+ const configDialog = document.getElementById('seed-config-dialog');
50
+ const configDialogTitle = document.getElementById('seed-config-dialog-title');
51
+ const configTextarea = document.getElementById('seed-config-textarea');
52
+ const configDialogCancel = document.getElementById('seed-config-dialog-cancel');
53
+ const configDialogApply = document.getElementById('seed-config-dialog-apply');
54
+
55
+ const truncateDialog = document.getElementById('seed-truncate-dialog');
56
+ const truncateCancel = document.getElementById('seed-truncate-cancel');
57
+ const truncateConfirm = document.getElementById('seed-truncate-confirm');
58
+
59
+ // State
60
+ const state = {};
61
+ let lastGeneratedResult = null;
32
62
 
33
63
  function escapeHtml(s) {
34
64
  return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
35
65
  }
36
66
 
37
- // Per-column live state: { strategy, ...optionValues }.
38
- const state = {};
67
+ // ---- Client-side Sample Generator (Fast Live Previews) --------------------
68
+
69
+ const SAMPLE_FIRST = ["Sombat", "Minh", "Hiroshi", "William", "Ravi", "Zara", "Elena", "Liam", "Mei", "Noah"];
70
+ const SAMPLE_LAST = ["Chen", "Smith", "Thompson", "Shah", "Yamamoto", "Patel", "Garcia", "Kim", "Nguyen", "Davis"];
71
+ const SAMPLE_DOMAINS = ["example.com", "acme.io", "tech.asia", "gmail.com", "startup.io"];
72
+ const SAMPLE_CITIES = ["New York", "Tokyo", "London", "Singapore", "Paris", "Berlin", "Sydney", "Toronto"];
73
+ const SAMPLE_COUNTRIES = ["United States", "Japan", "United Kingdom", "Singapore", "Germany", "France", "Canada", "Australia"];
74
+ const SAMPLE_COMPANIES = ["Apex Solutions", "Vortex Digital", "Nexus Systems", "Horizon Labs", "Quantum Dynamics", "Zenith Global"];
75
+ const SAMPLE_JOBS = ["Software Engineer", "Product Manager", "UX Designer", "DevOps Specialist", "Data Scientist", "Marketing Director"];
76
+ const SAMPLE_STATUSES = ["active", "pending", "completed", "archived", "draft", "verified"];
77
+ const SAMPLE_CURRENCIES = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"];
78
+ const SAMPLE_STATES = ["California", "New York", "Texas", "Washington", "Florida", "Illinois"];
79
+ const SAMPLE_WORDS = ["alpha", "beta", "stream", "cloud", "service", "cluster", "record", "node", "signal"];
80
+
81
+ function samplePick(arr) {
82
+ return arr[Math.floor(Math.random() * arr.length)];
83
+ }
84
+
85
+ function sampleInt(min, max) {
86
+ return Math.floor(Math.random() * (max - min + 1)) + min;
87
+ }
88
+
89
+ function samplePattern(pattern) {
90
+ if (!pattern) return 'INV-2026-' + sampleInt(1000, 9999);
91
+ const now = new Date();
92
+ let s = pattern
93
+ .replace(/\{YYYY\}/g, String(now.getFullYear()))
94
+ .replace(/\{YY\}/g, String(now.getFullYear()).slice(-2))
95
+ .replace(/\{MM\}/g, String(sampleInt(1, 12)).padStart(2, '0'))
96
+ .replace(/\{DD\}/g, String(sampleInt(1, 28)).padStart(2, '0'));
97
+ let res = '';
98
+ const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
99
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
100
+ for (let i = 0; i < s.length; i++) {
101
+ const ch = s[i];
102
+ if (ch === '#') res += String(sampleInt(0, 9));
103
+ else if (ch === 'A' || ch === '?') res += upper[sampleInt(0, upper.length - 1)];
104
+ else if (ch === 'a') res += lower[sampleInt(0, lower.length - 1)];
105
+ else res += ch;
106
+ }
107
+ return res;
108
+ }
39
109
 
40
- // ---- Option field definitions per strategy -----------------------------
110
+ function computeSampleValue(col, plan) {
111
+ const st = plan.strategy;
112
+ if (st === 'skip') return '(omitted / default)';
113
+ if (st === 'null') return 'NULL';
114
+ if (plan.nullPct && sampleInt(1, 100) <= plan.nullPct && !col.pk && !col.notnull) {
115
+ return 'NULL (simulated)';
116
+ }
117
+
118
+ let val = '';
119
+ switch (st) {
120
+ case 'fixed':
121
+ val = plan.value || '';
122
+ break;
123
+ case 'list': {
124
+ const vals = (plan.values || []).filter((v) => String(v).trim() !== '');
125
+ val = vals.length ? samplePick(vals) : '(empty list)';
126
+ break;
127
+ }
128
+ case 'pattern':
129
+ val = samplePattern(plan.pattern);
130
+ break;
131
+ case 'first':
132
+ val = samplePick(SAMPLE_FIRST);
133
+ break;
134
+ case 'last':
135
+ val = samplePick(SAMPLE_LAST);
136
+ break;
137
+ case 'fullname':
138
+ val = samplePick(SAMPLE_FIRST) + ' ' + samplePick(SAMPLE_LAST);
139
+ break;
140
+ case 'username':
141
+ val = samplePick(SAMPLE_FIRST).toLowerCase() + sampleInt(10, 999);
142
+ break;
143
+ case 'job':
144
+ val = samplePick(SAMPLE_JOBS);
145
+ break;
146
+ case 'company':
147
+ val = samplePick(SAMPLE_COMPANIES);
148
+ break;
149
+ case 'currency':
150
+ val = samplePick(SAMPLE_CURRENCIES);
151
+ break;
152
+ case 'status':
153
+ val = samplePick(SAMPLE_STATUSES);
154
+ break;
155
+ case 'creditCard':
156
+ val = '4532-****-****-' + sampleInt(1000, 9999);
157
+ break;
158
+ case 'avatar':
159
+ val = 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + samplePick(SAMPLE_FIRST).toLowerCase();
160
+ break;
161
+ case 'color':
162
+ val = plan.format === 'name' ? 'Emerald' : '#3b82f6';
163
+ break;
164
+ case 'email':
165
+ val = samplePick(SAMPLE_FIRST).toLowerCase() + '.' + samplePick(SAMPLE_LAST).toLowerCase() + '@' + samplePick(SAMPLE_DOMAINS);
166
+ break;
167
+ case 'phone':
168
+ val = '+1 (' + sampleInt(200, 999) + ') ' + sampleInt(200, 999) + '-' + sampleInt(1000, 9999);
169
+ break;
170
+ case 'city':
171
+ val = samplePick(SAMPLE_CITIES);
172
+ break;
173
+ case 'state':
174
+ val = samplePick(SAMPLE_STATES);
175
+ break;
176
+ case 'country':
177
+ val = samplePick(SAMPLE_COUNTRIES);
178
+ break;
179
+ case 'countryCode':
180
+ val = samplePick(['US', 'GB', 'CA', 'DE', 'JP', 'FR', 'AU', 'SG']);
181
+ break;
182
+ case 'postal':
183
+ val = String(sampleInt(10000, 99999));
184
+ break;
185
+ case 'address':
186
+ val = sampleInt(100, 9999) + ' ' + samplePick(SAMPLE_WORDS) + ' Ave, ' + samplePick(SAMPLE_CITIES);
187
+ break;
188
+ case 'url':
189
+ val = 'https://www.' + samplePick(SAMPLE_DOMAINS) + '/' + samplePick(SAMPLE_WORDS);
190
+ break;
191
+ case 'domain':
192
+ val = samplePick(SAMPLE_DOMAINS);
193
+ break;
194
+ case 'ip':
195
+ val = sampleInt(1, 223) + '.' + sampleInt(0, 255) + '.' + sampleInt(0, 255) + '.' + sampleInt(1, 254);
196
+ break;
197
+ case 'mac':
198
+ val = '00:1A:2B:' + sampleInt(10, 99) + ':' + sampleInt(10, 99) + ':' + sampleInt(10, 99);
199
+ break;
200
+ case 'uuid':
201
+ val = '550e8400-e29b-41d4-a716-' + sampleInt(100000000000, 999999999999);
202
+ break;
203
+ case 'slug':
204
+ val = samplePick(SAMPLE_WORDS) + '-' + samplePick(SAMPLE_WORDS) + '-' + sampleInt(100, 999);
205
+ break;
206
+ case 'words':
207
+ val = samplePick(SAMPLE_WORDS) + ' ' + samplePick(SAMPLE_WORDS);
208
+ break;
209
+ case 'sentence':
210
+ val = 'Lorem ipsum dolor sit ' + samplePick(SAMPLE_WORDS) + ' amet.';
211
+ break;
212
+ case 'paragraph':
213
+ val = 'Lorem ipsum dolor sit amet. Consectetur adipiscing elit ' + samplePick(SAMPLE_WORDS) + ' dynamicus.';
214
+ break;
215
+ case 'latitude':
216
+ val = (Math.random() * 180 - 90).toFixed(6);
217
+ break;
218
+ case 'longitude':
219
+ val = (Math.random() * 360 - 180).toFixed(6);
220
+ break;
221
+ case 'sequence':
222
+ val = String((plan.start || 1));
223
+ break;
224
+ case 'int':
225
+ val = String(sampleInt(plan.min ?? 1, plan.max ?? 1000));
226
+ break;
227
+ case 'decimal': {
228
+ const min = plan.min ?? 1;
229
+ const max = plan.max ?? 1000;
230
+ const prec = plan.precision ?? 2;
231
+ val = (Math.random() * (max - min) + min).toFixed(prec);
232
+ break;
233
+ }
234
+ case 'date':
235
+ val = '2026-03-' + String(sampleInt(1, 28)).padStart(2, '0');
236
+ break;
237
+ case 'datetime':
238
+ val = '2026-03-' + String(sampleInt(1, 28)).padStart(2, '0') + 'T14:30:00.000Z';
239
+ break;
240
+ case 'time':
241
+ val = '14:' + String(sampleInt(10, 59)) + ':00';
242
+ break;
243
+ case 'timestampUnix':
244
+ val = plan.format === 'ms' ? String(Date.now()) : String(Math.floor(Date.now() / 1000));
245
+ break;
246
+ case 'bool':
247
+ val = samplePick(['1', '0']);
248
+ break;
249
+ case 'bytes':
250
+ val = '0x' + sampleInt(100000, 999999).toString(16);
251
+ break;
252
+ case 'json':
253
+ val = '{"scope":"read-only","enabled":true}';
254
+ break;
255
+ case 'hash':
256
+ val = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'.slice(0, plan.maxLen || 32);
257
+ break;
258
+ case 'token':
259
+ val = 'tk_' + sampleInt(100000, 999999);
260
+ break;
261
+ case 'fk':
262
+ val = col.fk ? '[Sample from ' + col.fk.table + ']' : 'NULL';
263
+ break;
264
+ default:
265
+ val = '(auto)';
266
+ }
267
+
268
+ if (plan.prefix || plan.suffix) {
269
+ val = (plan.prefix || '') + val + (plan.suffix || '');
270
+ }
271
+ return val;
272
+ }
273
+
274
+ // ---- Field Definitions per Strategy -------------------------------------
41
275
 
42
276
  function field(key, label, type, attrs) {
43
277
  return Object.assign({ key, label, type }, attrs || {});
44
278
  }
279
+
45
280
  const FIELD_DEFS = {
46
281
  fixed: [field('value', 'Value', 'text')],
47
282
  list: [field('values', 'One value per line', 'textarea')],
283
+ pattern: [field('pattern', 'Pattern (tokens: #=digit, ?=letter, {YYYY})', 'text', { placeholder: 'INV-{YYYY}-####' })],
48
284
  int: [field('min', 'Min', 'number'), field('max', 'Max', 'number')],
49
- decimal: [field('min', 'Min', 'number'), field('max', 'Max', 'number'), field('precision', 'Decimals', 'number', { step: 1 })],
285
+ decimal: [field('min', 'Min', 'number'), field('max', 'Max', 'number'), field('precision', 'Decimals', 'number', { step: 1, min: 0, max: 10 })],
286
+ sequence: [field('start', 'Start from', 'number', { placeholder: '1' }), field('step', 'Step increment', 'number', { placeholder: '1' })],
50
287
  date: [field('from', 'From', 'date'), field('to', 'To', 'date')],
51
288
  datetime: [field('from', 'From', 'datetime-local'), field('to', 'To', 'datetime-local')],
52
289
  time: [],
290
+ timestampUnix: [field('format', 'Format (sec / ms)', 'select', { options: [{ value: 'sec', label: 'Seconds (10-digit)' }, { value: 'ms', label: 'Milliseconds (13-digit)' }] })],
291
+ color: [field('format', 'Color format', 'select', { options: [{ value: 'hex', label: 'Hex (#3b82f6)' }, { value: 'name', label: 'Color Name (Emerald)' }] })],
292
+ latitude: [field('min', 'Min Lat', 'number', { step: 0.0001, placeholder: '-90' }), field('max', 'Max Lat', 'number', { step: 0.0001, placeholder: '90' })],
293
+ longitude: [field('min', 'Min Lng', 'number', { step: 0.0001, placeholder: '-180' }), field('max', 'Max Lng', 'number', { step: 0.0001, placeholder: '180' })],
53
294
  words: [field('minLen', 'Min words', 'number'), field('maxLen', 'Max words', 'number')],
54
- sentence: [field('minLen', 'Min', 'number'), field('maxLen', 'Max', 'number')],
295
+ sentence: [field('minLen', 'Min sentences', 'number'), field('maxLen', 'Max sentences', 'number')],
296
+ paragraph: [],
55
297
  bytes: [field('minLen', 'Min bytes', 'number'), field('maxLen', 'Max bytes', 'number')],
56
298
  json: [field('jsonKeys', 'JSON keys (comma-separated)', 'text')],
57
299
  hash: [field('minLen', 'Min length', 'number'), field('maxLen', 'Max length', 'number')],
58
300
  token: [field('minLen', 'Min length', 'number'), field('maxLen', 'Max length', 'number')],
59
301
  };
60
302
 
303
+ const CATEGORY_TITLES = {
304
+ identity: '👤 Identity & People',
305
+ contact_web: '🌐 Contact & Web',
306
+ commerce: '🏢 Commerce & Business',
307
+ location: '📍 Location',
308
+ numeric: '🔢 Numbers & Math',
309
+ datetime: '📅 Date & Time',
310
+ system_crypto: '🔒 System & Crypto',
311
+ custom_control: '⚙️ Custom & Control',
312
+ relational: '🔗 Relational (Foreign Key)',
313
+ };
314
+
61
315
  function strategyNote(col, strategy) {
62
- if (strategy === 'fk' && col.fk) return 'Values are sampled from ' + escapeHtml(col.fk.table) + (col.fk.to ? '.' + escapeHtml(col.fk.to) : '') + '.';
63
- if (strategy === 'skip') return 'Column is omitted — the DB default (or NULL) applies.';
64
- if (strategy === 'null') return 'Column is explicitly set to NULL.';
65
- if (strategy === 'json') return 'Generates a valid JSON object — needed when the column has a JSON index or JSON default.';
66
- if (strategy === 'hash') return 'Generates a random hexadecimal hash.';
67
- if (strategy === 'token') return 'Generates a random short token / code.';
316
+ if (strategy === 'fk' && col.fk) return 'Sampled from foreign table ' + escapeHtml(col.fk.table) + (col.fk.to ? '.' + escapeHtml(col.fk.to) : '') + '.';
317
+ if (strategy === 'skip') return 'Omitted from INSERT — DB default or NULL applies.';
318
+ if (strategy === 'null') return 'Explicitly set to NULL for all rows.';
319
+ if (strategy === 'pattern') return 'Replaces # with digits, ? with letters, and {YYYY} with year.';
320
+ if (strategy === 'sequence') return 'Generates sequentially incrementing integers across rows.';
321
+ if (strategy === 'json') return 'Generates valid JSON objects compliant with indexes and defaults.';
322
+ if (strategy === 'hash') return 'Generates random cryptographic hex hashes.';
323
+ if (strategy === 'token') return 'Generates short alphanumeric identifier tokens.';
68
324
  return '';
69
325
  }
70
326
 
71
- // ---- Card building ------------------------------------------------------
327
+ // ---- Card Building ------------------------------------------------------
72
328
 
73
- function buildOptionInputs(container, col, plan) {
329
+ function updateCardSample(col, card) {
330
+ const sampleEl = card.querySelector('.seed-sample-value');
331
+ if (sampleEl) {
332
+ const sample = computeSampleValue(col, state[col.name] || {});
333
+ sampleEl.textContent = sample;
334
+ }
335
+ }
336
+
337
+ function buildOptionInputs(container, col, plan, card) {
74
338
  container.innerHTML = '';
75
339
  const fields = FIELD_DEFS[plan.strategy] || [];
76
- if (!fields.length) return;
77
- const grid = document.createElement('div');
78
- grid.className = 'grid grid-cols-2 gap-2';
79
- fields.forEach((f) => {
80
- const wrap = document.createElement('label');
81
- wrap.className = 'block';
82
- const label = document.createElement('span');
83
- label.className = 'field-label !mb-1 !text-[11px]';
84
- label.textContent = f.label;
85
- wrap.appendChild(label);
86
- let el;
87
- if (f.type === 'textarea') {
88
- el = document.createElement('textarea');
89
- el.rows = 4;
90
- el.className = 'field-input col-span-2 w-full font-mono !text-[12px]';
91
- el.placeholder = 'one value per line';
92
- el.spellcheck = false;
93
- if (Array.isArray(plan.values)) el.value = plan.values.join('\n');
94
- } else {
95
- el = document.createElement('input');
96
- el.type = f.type;
97
- el.className = 'field-input !py-1.5 !text-[13px]';
98
- if (f.step != null) el.step = String(f.step);
99
- const v = plan[f.key];
100
- if (f.key === 'jsonKeys' && Array.isArray(v)) {
101
- el.value = v.join(', ');
102
- } else if (v !== undefined && v !== null) {
103
- el.value = String(v);
104
- }
105
- }
106
- el.dataset.optionKey = f.key;
107
- el.addEventListener('input', () => {
108
- const key = el.dataset.optionKey;
340
+
341
+ const hasOptions = fields.length > 0;
342
+ const isNullable = !col.pk && !col.notnull;
343
+ const isTextish = !['int', 'decimal', 'bool', 'bytes', 'skip', 'null'].includes(plan.strategy);
344
+
345
+ if (!hasOptions && !isNullable) return;
346
+
347
+ const wrap = document.createElement('div');
348
+ wrap.className = 'space-y-2 pt-2 border-t border-base-200/60';
349
+
350
+ if (fields.length) {
351
+ const grid = document.createElement('div');
352
+ grid.className = 'grid grid-cols-2 gap-2';
353
+ fields.forEach((f) => {
354
+ const fieldWrap = document.createElement('label');
355
+ fieldWrap.className = 'block' + (f.type === 'textarea' || f.key === 'pattern' || f.key === 'jsonKeys' ? ' col-span-2' : '');
356
+ const label = document.createElement('span');
357
+ label.className = 'field-label !mb-1 !text-[11px] font-medium text-base-content/70';
358
+ label.textContent = f.label;
359
+ fieldWrap.appendChild(label);
360
+
361
+ let el;
109
362
  if (f.type === 'textarea') {
110
- state[col.name][key] = el.value.split('\n').map((s) => s.trim()).filter(Boolean);
111
- } else if (key === 'jsonKeys') {
112
- state[col.name][key] = el.value.split(/[,;]/).map((s) => s.trim()).filter(Boolean);
113
- } else if (key === 'value' || key === 'from' || key === 'to') {
114
- state[col.name][key] = el.value;
363
+ el = document.createElement('textarea');
364
+ el.rows = 3;
365
+ el.className = 'field-input w-full font-mono !text-[11px] leading-tight';
366
+ el.placeholder = 'one value per line';
367
+ el.spellcheck = false;
368
+ if (Array.isArray(plan.values)) el.value = plan.values.join('\n');
369
+ } else if (f.type === 'select') {
370
+ el = document.createElement('select');
371
+ el.className = 'field-select !py-1 !text-[12px]';
372
+ (f.options || []).forEach((opt) => {
373
+ const o = document.createElement('option');
374
+ o.value = opt.value;
375
+ o.textContent = opt.label;
376
+ if (plan[f.key] === opt.value) o.selected = true;
377
+ el.appendChild(o);
378
+ });
115
379
  } else {
116
- const n = Number(el.value);
117
- state[col.name][key] = el.value === '' || !Number.isFinite(n) ? undefined : n;
380
+ el = document.createElement('input');
381
+ el.type = f.type;
382
+ el.className = 'field-input !py-1 !text-[12px]';
383
+ if (f.step != null) el.step = String(f.step);
384
+ if (f.min != null) el.min = String(f.min);
385
+ if (f.max != null) el.max = String(f.max);
386
+ if (f.placeholder) el.placeholder = f.placeholder;
387
+ const v = plan[f.key];
388
+ if (f.key === 'jsonKeys' && Array.isArray(v)) {
389
+ el.value = v.join(', ');
390
+ } else if (v !== undefined && v !== null) {
391
+ el.value = String(v);
392
+ }
118
393
  }
394
+
395
+ el.dataset.optionKey = f.key;
396
+ el.addEventListener('input', () => {
397
+ const key = el.dataset.optionKey;
398
+ if (f.type === 'textarea') {
399
+ state[col.name][key] = el.value.split('\n').map((s) => s.trim()).filter(Boolean);
400
+ } else if (key === 'jsonKeys') {
401
+ state[col.name][key] = el.value.split(/[,;]/).map((s) => s.trim()).filter(Boolean);
402
+ } else if (key === 'value' || key === 'from' || key === 'to' || key === 'pattern' || key === 'format') {
403
+ state[col.name][key] = el.value;
404
+ } else {
405
+ const n = Number(el.value);
406
+ state[col.name][key] = el.value === '' || !Number.isFinite(n) ? undefined : n;
407
+ }
408
+ updateCardSample(col, card);
409
+ });
410
+
411
+ fieldWrap.appendChild(el);
412
+ grid.appendChild(fieldWrap);
119
413
  });
120
- wrap.appendChild(el);
121
- grid.appendChild(wrap);
122
- });
123
- container.appendChild(grid);
414
+ wrap.appendChild(grid);
415
+ }
416
+
417
+ // Null percentage simulation for nullable columns
418
+ if (isNullable && plan.strategy !== 'skip' && plan.strategy !== 'null') {
419
+ const nullRow = document.createElement('div');
420
+ nullRow.className = 'flex items-center justify-between gap-2 pt-1.5 text-[11px]';
421
+ nullRow.innerHTML =
422
+ '<span class="text-base-content/60">Simulate Nulls:</span>' +
423
+ '<div class="flex items-center gap-1.5">' +
424
+ '<input type="range" min="0" max="100" step="5" value="' + (plan.nullPct || 0) + '" class="range range-xs range-primary w-24" />' +
425
+ '<span class="font-mono text-[11px] w-8 text-right text-base-content/75">' + (plan.nullPct || 0) + '%</span>' +
426
+ '</div>';
427
+ const rangeInput = nullRow.querySelector('input[type="range"]');
428
+ const valSpan = nullRow.querySelector('span:last-child');
429
+ rangeInput.addEventListener('input', () => {
430
+ const val = Number(rangeInput.value);
431
+ valSpan.textContent = val + '%';
432
+ state[col.name].nullPct = val > 0 ? val : undefined;
433
+ updateCardSample(col, card);
434
+ });
435
+ wrap.appendChild(nullRow);
436
+ }
437
+
438
+ container.appendChild(wrap);
124
439
  }
125
440
 
126
441
  function buildCard(col) {
127
442
  const card = document.createElement('div');
128
- card.className = 'app-card p-4';
443
+ card.className = 'app-card p-4 flex flex-col justify-between transition-shadow hover:shadow-md';
129
444
  card.dataset.column = col.name;
445
+ card.dataset.type = (col.type || '').toLowerCase();
446
+
447
+ // Top Header
448
+ const top = document.createElement('div');
449
+ top.className = 'space-y-3';
130
450
 
131
451
  const head = document.createElement('div');
132
- head.className = 'flex flex-wrap items-start justify-between gap-2';
452
+ head.className = 'flex items-start justify-between gap-2';
133
453
 
134
- const title = document.createElement('div');
135
- title.className = 'min-w-0';
136
- const name = document.createElement('p');
137
- name.className = 'flex flex-wrap items-center gap-1.5 font-mono text-sm font-semibold';
454
+ const titleBox = document.createElement('div');
455
+ titleBox.className = 'min-w-0';
456
+ const nameRow = document.createElement('div');
457
+ nameRow.className = 'flex flex-wrap items-center gap-1.5';
458
+ const name = document.createElement('span');
459
+ name.className = 'font-mono text-sm font-bold tracking-tight text-base-content';
138
460
  name.textContent = col.name;
461
+ nameRow.appendChild(name);
462
+
139
463
  if (col.pk) {
140
464
  const chip = document.createElement('span');
141
- chip.className = 'chip chip-pk';
465
+ chip.className = 'badge badge-xs badge-warning font-mono font-bold';
142
466
  chip.textContent = 'PK';
143
- name.appendChild(chip);
467
+ nameRow.appendChild(chip);
144
468
  }
145
469
  if (col.notnull) {
146
470
  const chip = document.createElement('span');
147
- chip.className = 'chip chip-type';
471
+ chip.className = 'badge badge-xs badge-info font-mono';
148
472
  chip.textContent = 'NOT NULL';
149
- name.appendChild(chip);
473
+ nameRow.appendChild(chip);
474
+ } else {
475
+ const chip = document.createElement('span');
476
+ chip.className = 'badge badge-xs badge-ghost text-base-content/40 font-mono';
477
+ chip.textContent = 'NULLABLE';
478
+ nameRow.appendChild(chip);
479
+ }
480
+ if (col.unique) {
481
+ const chip = document.createElement('span');
482
+ chip.className = 'badge badge-xs badge-secondary font-mono';
483
+ chip.textContent = 'UNIQUE';
484
+ nameRow.appendChild(chip);
150
485
  }
151
486
  if (col.hasDefault) {
152
487
  const chip = document.createElement('span');
153
- chip.className = 'chip chip-type';
154
- chip.textContent = 'default';
155
- name.appendChild(chip);
488
+ chip.className = 'badge badge-xs badge-ghost font-mono';
489
+ chip.textContent = 'DEFAULT';
490
+ nameRow.appendChild(chip);
491
+ }
492
+ if (col.fk) {
493
+ const chip = document.createElement('span');
494
+ chip.className = 'badge badge-xs badge-accent font-mono';
495
+ chip.textContent = 'FK → ' + col.fk.table;
496
+ nameRow.appendChild(chip);
156
497
  }
157
- const type = document.createElement('p');
158
- type.className = 'mt-0.5 text-xs text-base-content/45';
159
- type.textContent = col.type || 'untyped';
160
- title.appendChild(name);
161
- title.appendChild(type);
162
- head.appendChild(title);
163
498
 
499
+ const typeDesc = document.createElement('p');
500
+ typeDesc.className = 'mt-0.5 text-[11px] font-mono text-base-content/45';
501
+ typeDesc.textContent = col.type || 'untyped';
502
+
503
+ titleBox.appendChild(nameRow);
504
+ titleBox.appendChild(typeDesc);
505
+ head.appendChild(titleBox);
506
+ top.appendChild(head);
507
+
508
+ // Strategy Dropdown with Categorized <optgroup>s
164
509
  const select = document.createElement('select');
165
- select.className = 'field-select seed-strategy !w-auto !py-1 !text-[13px]';
510
+ select.className = 'field-select seed-strategy w-full !py-1.5 !text-xs font-medium';
166
511
  select.dataset.column = col.name;
512
+
513
+ const grouped = {};
167
514
  (col.strategies || []).forEach((s) => {
168
- const opt = document.createElement('option');
169
- opt.value = s.id;
170
- opt.textContent = s.label;
171
- select.appendChild(opt);
515
+ const cat = s.category || 'custom_control';
516
+ if (!grouped[cat]) grouped[cat] = [];
517
+ grouped[cat].push(s);
172
518
  });
173
- head.appendChild(select);
174
- card.appendChild(head);
175
519
 
520
+ Object.keys(grouped).forEach((cat) => {
521
+ const grp = document.createElement('optgroup');
522
+ grp.label = CATEGORY_TITLES[cat] || cat;
523
+ grouped[cat].forEach((s) => {
524
+ const opt = document.createElement('option');
525
+ opt.value = s.id;
526
+ opt.textContent = s.label;
527
+ grp.appendChild(opt);
528
+ });
529
+ select.appendChild(grp);
530
+ });
531
+
532
+ top.appendChild(select);
533
+
534
+ // Live Sample Chip
535
+ const sampleBox = document.createElement('div');
536
+ sampleBox.className = 'flex items-center justify-between gap-2 px-2.5 py-1.5 rounded-lg bg-base-200/50 text-[11px]';
537
+ sampleBox.innerHTML =
538
+ '<div class="flex items-center gap-1.5 min-w-0 overflow-hidden text-ellipsis">' +
539
+ '<span class="text-base-content/50 shrink-0 font-medium">Sample:</span>' +
540
+ '<span class="seed-sample-value font-mono text-primary font-medium truncate">...</span>' +
541
+ '</div>' +
542
+ '<button type="button" class="seed-sample-refresh btn btn-ghost btn-xs btn-square h-5 w-5 shrink-0" title="Reroll sample">' +
543
+ '🎲' +
544
+ '</button>';
545
+
546
+ const sampleRefreshBtn = sampleBox.querySelector('.seed-sample-refresh');
547
+ sampleRefreshBtn.addEventListener('click', () => updateCardSample(col, card));
548
+ top.appendChild(sampleBox);
549
+
550
+ // Strategy-specific options area
176
551
  const options = document.createElement('div');
177
- options.className = 'seed-options mt-2.5';
178
- card.appendChild(options);
552
+ options.className = 'seed-options';
553
+ top.appendChild(options);
554
+
555
+ card.appendChild(top);
556
+
557
+ // Bottom notes & warnings
558
+ const bottom = document.createElement('div');
559
+ bottom.className = 'mt-3 pt-2';
179
560
 
180
561
  const note = document.createElement('p');
181
- note.className = 'seed-note mt-2 hidden text-[11px] leading-relaxed text-base-content/45';
182
- card.appendChild(note);
562
+ note.className = 'seed-note hidden text-[11px] leading-relaxed text-base-content/45';
563
+ bottom.appendChild(note);
183
564
 
184
565
  const warn = document.createElement('p');
185
- warn.className = 'seed-warn mt-2 hidden text-[11px] font-medium text-warning';
186
- card.appendChild(warn);
566
+ warn.className = 'seed-warn hidden text-[11px] font-medium text-warning flex items-center gap-1';
567
+ bottom.appendChild(warn);
187
568
 
188
- // Wire up strategy changes.
569
+ card.appendChild(bottom);
570
+
571
+ // Event listener for strategy selection change
189
572
  select.addEventListener('change', () => {
190
573
  const st = select.value;
191
574
  state[col.name] = Object.assign({}, state[col.name] || {}, { strategy: st });
192
- buildOptionInputs(options, col, state[col.name]);
575
+ buildOptionInputs(options, col, state[col.name], card);
193
576
  note.textContent = strategyNote(col, st);
194
577
  note.classList.toggle('hidden', !note.textContent);
195
578
  updateWarn(col, warn);
579
+ updateCardSample(col, card);
196
580
  });
197
581
 
198
582
  columnsBox.appendChild(card);
@@ -203,7 +587,7 @@
203
587
  const st = state[col.name] ? state[col.name].strategy : null;
204
588
  const risky = (st === 'skip' || st === 'null') && !col.pk && col.notnull && !col.hasDefault;
205
589
  warnEl.textContent = risky
206
- ? 'This column is NOT NULL with no default — "' + st + '" rows may fail to insert.'
590
+ ? '⚠️ Column is NOT NULL with no default — "' + st + '" rows may fail.'
207
591
  : '';
208
592
  warnEl.classList.toggle('hidden', !warnEl.textContent);
209
593
  }
@@ -211,19 +595,85 @@
211
595
  function resetCard(col) {
212
596
  const plan = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
213
597
  state[col.name] = plan;
214
- const select = columnsBox.querySelector('.seed-strategy[data-column="' + CSS.escape(col.name) + '"]');
215
- if (select) {
216
- select.value = plan.strategy;
217
- const options = select.closest('.app-card').querySelector('.seed-options');
218
- const note = select.closest('.app-card').querySelector('.seed-note');
219
- const warn = select.closest('.app-card').querySelector('.seed-warn');
220
- buildOptionInputs(options, col, plan);
598
+ const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
599
+ if (card) {
600
+ const select = card.querySelector('.seed-strategy');
601
+ const options = card.querySelector('.seed-options');
602
+ const note = card.querySelector('.seed-note');
603
+ const warn = card.querySelector('.seed-warn');
604
+ if (select) select.value = plan.strategy;
605
+ buildOptionInputs(options, col, plan, card);
221
606
  note.textContent = strategyNote(col, plan.strategy);
222
607
  note.classList.toggle('hidden', !note.textContent);
223
608
  updateWarn(col, warn);
609
+ updateCardSample(col, card);
224
610
  }
225
611
  }
226
612
 
613
+ // ---- Filtering / Searching ----------------------------------------------
614
+
615
+ function filterColumns() {
616
+ const q = (searchInput ? searchInput.value : '').toLowerCase().trim();
617
+ let visible = 0;
618
+ columns.forEach((col) => {
619
+ const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
620
+ if (!card) return;
621
+ const match = !q || col.name.toLowerCase().includes(q) || (col.type || '').toLowerCase().includes(q);
622
+ card.classList.toggle('hidden', !match);
623
+ if (match) visible += 1;
624
+ });
625
+
626
+ if (emptySearchBox) emptySearchBox.classList.toggle('hidden', visible > 0);
627
+ if (colCountBadge) colCountBadge.textContent = visible === columns.length ? `${columns.length} columns` : `${visible} of ${columns.length} columns`;
628
+ }
629
+
630
+ // ---- Presets ------------------------------------------------------------
631
+
632
+ function applyPreset(type) {
633
+ columns.forEach((col) => {
634
+ if (type === 'realistic') {
635
+ resetCard(col);
636
+ } else if (type === 'minimal') {
637
+ if (!col.pk && !col.notnull && !col.hasDefault) {
638
+ state[col.name] = { strategy: 'null' };
639
+ } else {
640
+ state[col.name] = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
641
+ }
642
+ } else if (type === 'sparse') {
643
+ state[col.name] = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
644
+ if (!col.pk && !col.notnull) {
645
+ state[col.name].nullPct = 30;
646
+ }
647
+ } else if (type === 'numbered') {
648
+ const t = (col.type || '').toUpperCase();
649
+ if (t.startsWith('INTEGER')) {
650
+ state[col.name] = { strategy: 'sequence', start: 1, step: 1 };
651
+ } else if (t === 'TEXT' || t === '') {
652
+ state[col.name] = { strategy: 'pattern', pattern: 'REC-####' };
653
+ } else {
654
+ state[col.name] = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
655
+ }
656
+ }
657
+
658
+ // Sync DOM
659
+ const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
660
+ if (card) {
661
+ const select = card.querySelector('.seed-strategy');
662
+ const options = card.querySelector('.seed-options');
663
+ const note = card.querySelector('.seed-note');
664
+ const warn = card.querySelector('.seed-warn');
665
+ if (select) select.value = state[col.name].strategy;
666
+ buildOptionInputs(options, col, state[col.name], card);
667
+ note.textContent = strategyNote(col, state[col.name].strategy);
668
+ note.classList.toggle('hidden', !note.textContent);
669
+ updateWarn(col, warn);
670
+ updateCardSample(col, card);
671
+ }
672
+ });
673
+
674
+ if (window.UI && UI.showToast) UI.showToast('Applied preset: ' + type, 'info');
675
+ }
676
+
227
677
  // ---- Apply strategy to all ---------------------------------------------
228
678
 
229
679
  function unionStrategies() {
@@ -237,15 +687,15 @@
237
687
  dialog.className = 'modal';
238
688
  dialog.id = 'seed-apply-all-dialog';
239
689
  const inner = document.createElement('div');
240
- inner.className = 'modal-box app-card';
690
+ inner.className = 'modal-box app-card max-w-md';
241
691
  inner.innerHTML =
242
- '<h3 class="text-lg font-bold">Apply strategy to all columns</h3>' +
243
- '<p class="mt-1 text-sm text-base-content/55">Columns that do not support the chosen strategy are left unchanged. Existing option values are preserved where possible.</p>' +
244
- '<label class="field-label mt-4">Strategy</label>' +
245
- '<select id="seed-apply-all-select" class="field-select w-full"></select>' +
246
- '<div class="mt-6 flex justify-end gap-2">' +
247
- '<button id="seed-apply-all-cancel" type="button" class="btn btn-ghost">Cancel</button>' +
248
- '<button id="seed-apply-all-go" type="button" class="btn btn-primary">Apply</button>' +
692
+ '<h3 class="text-base font-bold">Apply strategy to all columns</h3>' +
693
+ '<p class="mt-1 text-xs text-base-content/60">Columns that do not support the chosen strategy are left untouched.</p>' +
694
+ '<label class="field-label mt-4 text-xs font-medium">Strategy</label>' +
695
+ '<select id="seed-apply-all-select" class="field-select w-full text-xs"></select>' +
696
+ '<div class="mt-5 flex justify-end gap-2">' +
697
+ '<button id="seed-apply-all-cancel" type="button" class="btn btn-ghost btn-sm">Cancel</button>' +
698
+ '<button id="seed-apply-all-go" type="button" class="btn btn-primary btn-sm">Apply to All</button>' +
249
699
  '</div>';
250
700
  dialog.appendChild(inner);
251
701
  document.body.appendChild(dialog);
@@ -266,27 +716,30 @@
266
716
  const supported = (col.strategies || []).some((s) => s.id === target);
267
717
  if (!supported) return;
268
718
  state[col.name] = Object.assign({}, state[col.name] || {}, { strategy: target });
269
- const sel = columnsBox.querySelector('.seed-strategy[data-column="' + CSS.escape(col.name) + '"]');
270
- if (sel) {
271
- sel.value = target;
272
- const options = sel.closest('.app-card').querySelector('.seed-options');
273
- const note = sel.closest('.app-card').querySelector('.seed-note');
274
- const warn = sel.closest('.app-card').querySelector('.seed-warn');
275
- buildOptionInputs(options, col, state[col.name]);
719
+ const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
720
+ if (card) {
721
+ const sel = card.querySelector('.seed-strategy');
722
+ const options = card.querySelector('.seed-options');
723
+ const note = card.querySelector('.seed-note');
724
+ const warn = card.querySelector('.seed-warn');
725
+ if (sel) sel.value = target;
726
+ buildOptionInputs(options, col, state[col.name], card);
276
727
  note.textContent = strategyNote(col, target);
277
728
  note.classList.toggle('hidden', !note.textContent);
278
729
  updateWarn(col, warn);
730
+ updateCardSample(col, card);
279
731
  }
280
732
  });
281
733
  close();
734
+ if (window.UI && UI.showToast) UI.showToast('Strategy applied across columns.', 'success');
282
735
  });
283
736
  return dialog;
284
737
  }
285
738
 
286
- // ---- Feedback -----------------------------------------------------------
739
+ // ---- Dual-Mode Preview Rendering ---------------------------------------
287
740
 
288
741
  function showFeedback(warnings) {
289
- feedback.classList.remove('hidden');
742
+ if (!feedback) return;
290
743
  if (!warnings || !warnings.length) {
291
744
  feedback.innerHTML = '';
292
745
  feedback.classList.add('hidden');
@@ -295,17 +748,136 @@
295
748
  feedback.innerHTML =
296
749
  '<div class="alert alert-warning shadow-soft">' +
297
750
  '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>' +
298
- '<div class="text-sm">' + warnings.map((w) => '<p>' + escapeHtml(w) + '</p>').join('') + '</div>' +
751
+ '<div class="text-xs space-y-1">' + warnings.map((w) => '<p>' + escapeHtml(w) + '</p>').join('') + '</div>' +
299
752
  '</div>';
753
+ feedback.classList.remove('hidden');
754
+ }
755
+
756
+ function renderPreview(data) {
757
+ lastGeneratedResult = data;
758
+ if (!previewCard) return;
759
+
760
+ previewCard.classList.remove('hidden');
761
+ if (previewSummaryBadge) {
762
+ previewSummaryBadge.textContent = `${data.count} row(s) generated`;
763
+ }
764
+
765
+ // 1. SQL View
766
+ if (sqlCode) sqlCode.textContent = data.sql || '';
767
+
768
+ // 2. Table View
769
+ if (previewThead && previewTbody) {
770
+ previewThead.innerHTML = '';
771
+ previewTbody.innerHTML = '';
772
+
773
+ const trHead = document.createElement('tr');
774
+ const thIdx = document.createElement('th');
775
+ thIdx.className = 'w-12 text-center text-base-content/40';
776
+ thIdx.textContent = '#';
777
+ trHead.appendChild(thIdx);
778
+
779
+ columns.forEach((col) => {
780
+ const th = document.createElement('th');
781
+ th.className = 'whitespace-nowrap font-mono';
782
+ th.innerHTML = escapeHtml(col.name) + ' <span class="text-[10px] text-base-content/40 font-normal">(' + escapeHtml(col.type || 'text') + ')</span>';
783
+ trHead.appendChild(th);
784
+ });
785
+ previewThead.appendChild(trHead);
786
+
787
+ const rows = data.previewRows || [];
788
+ rows.forEach((row, i) => {
789
+ const tr = document.createElement('tr');
790
+ const tdIdx = document.createElement('td');
791
+ tdIdx.className = 'text-center font-mono text-[10px] text-base-content/40';
792
+ tdIdx.textContent = String(i + 1);
793
+ tr.appendChild(tdIdx);
794
+
795
+ columns.forEach((col) => {
796
+ const td = document.createElement('td');
797
+ td.className = 'font-mono whitespace-nowrap max-w-xs truncate';
798
+ const val = row[col.name];
799
+ if (val === null) {
800
+ td.innerHTML = '<span class="badge badge-xs badge-ghost text-base-content/40">NULL</span>';
801
+ } else if (val === '(default)') {
802
+ td.innerHTML = '<span class="badge badge-xs badge-outline text-base-content/40">DEFAULT</span>';
803
+ } else if (typeof val === 'boolean') {
804
+ td.innerHTML = val ? '<span class="badge badge-xs badge-success">TRUE</span>' : '<span class="badge badge-xs badge-ghost">FALSE</span>';
805
+ } else {
806
+ td.textContent = String(val);
807
+ }
808
+ tr.appendChild(td);
809
+ });
810
+ previewTbody.appendChild(tr);
811
+ });
812
+ }
813
+
814
+ previewCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
300
815
  }
301
816
 
302
- function showSql(sql) {
303
- sqlCode.textContent = sql;
304
- sqlBox.classList.remove('hidden');
305
- sqlBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
817
+ function setPreviewTab(tab) {
818
+ if (tab === 'table') {
819
+ tabTableView.classList.add('tab-active');
820
+ tabSqlView.classList.remove('tab-active');
821
+ tableViewContainer.classList.remove('hidden');
822
+ sqlViewContainer.classList.add('hidden');
823
+ } else {
824
+ tabSqlView.classList.add('tab-active');
825
+ tabTableView.classList.remove('tab-active');
826
+ sqlViewContainer.classList.remove('hidden');
827
+ tableViewContainer.classList.add('hidden');
828
+ }
306
829
  }
307
830
 
308
- // ---- Collect plan + submit ---------------------------------------------
831
+ // ---- Export Handlers ----------------------------------------------------
832
+
833
+ function downloadFile(content, filename, mime) {
834
+ const blob = new Blob([content], { type: mime });
835
+ const url = URL.createObjectURL(blob);
836
+ const a = document.createElement('a');
837
+ a.href = url;
838
+ a.download = filename;
839
+ document.body.appendChild(a);
840
+ a.click();
841
+ document.body.removeChild(a);
842
+ URL.revokeObjectURL(url);
843
+ }
844
+
845
+ function exportCsv() {
846
+ if (!lastGeneratedResult || !lastGeneratedResult.previewRows) {
847
+ if (window.UI && UI.showError) UI.showError('Generate preview data first.');
848
+ return;
849
+ }
850
+ const rows = lastGeneratedResult.previewRows;
851
+ const colNames = columns.map((c) => c.name);
852
+ let csv = colNames.map((c) => `"${c.replace(/"/g, '""')}"`).join(',') + '\n';
853
+ rows.forEach((r) => {
854
+ csv += colNames.map((c) => {
855
+ const v = r[c];
856
+ if (v == null) return '';
857
+ return `"${String(v).replace(/"/g, '""')}"`;
858
+ }).join(',') + '\n';
859
+ });
860
+ downloadFile(csv, `${cfg.table}_seed_data.csv`, 'text/csv;charset=utf-8;');
861
+ }
862
+
863
+ function exportJson() {
864
+ if (!lastGeneratedResult || !lastGeneratedResult.previewRows) {
865
+ if (window.UI && UI.showError) UI.showError('Generate preview data first.');
866
+ return;
867
+ }
868
+ const jsonStr = JSON.stringify(lastGeneratedResult.previewRows, null, 2);
869
+ downloadFile(jsonStr, `${cfg.table}_seed_data.json`, 'application/json;charset=utf-8;');
870
+ }
871
+
872
+ function downloadSql() {
873
+ if (!lastGeneratedResult || !lastGeneratedResult.sql) {
874
+ if (window.UI && UI.showError) UI.showError('Generate preview data first.');
875
+ return;
876
+ }
877
+ downloadFile(lastGeneratedResult.sql, `${cfg.table}_seed.sql`, 'text/sql;charset=utf-8;');
878
+ }
879
+
880
+ // ---- Collect Plan + Submit ----------------------------------------------
309
881
 
310
882
  function collectPlan() {
311
883
  const plan = {};
@@ -327,21 +899,43 @@
327
899
  return Math.max(1, Math.min(cfg.maxRows || 5000, Number.isFinite(n) ? n : 10));
328
900
  }
329
901
 
330
- async function submit(insert) {
902
+ async function executeSeed(truncate = false) {
331
903
  const count = currentCount();
332
904
  const plan = collectPlan();
333
- const path = insert ? '/api/tables/' + t + '/seed' : '/api/tables/' + t + '/seed/generate';
905
+ const path = '/api/tables/' + t + '/seed';
334
906
  try {
335
- const data = await Api.post(path, { count, plan });
336
- if (insert) {
337
- UI.showToast(data.message, 'success');
338
- showFeedback(data.warnings || []);
339
- } else {
340
- showSql(data.sql);
341
- showFeedback(data.warnings || []);
907
+ if (insertBtn) {
908
+ insertBtn.disabled = true;
909
+ insertBtn.classList.add('loading');
342
910
  }
911
+ const data = await Api.post(path, { count, plan, truncate });
912
+ if (window.UI && UI.showToast) {
913
+ UI.showToast(data.message || `Inserted ${data.inserted} row(s) successfully.`, 'success');
914
+ }
915
+ showFeedback(data.warnings || []);
343
916
  } catch (e) {
344
- UI.showError(e.message);
917
+ if (window.UI && UI.showError) UI.showError(e.message);
918
+ } finally {
919
+ if (insertBtn) {
920
+ insertBtn.disabled = false;
921
+ insertBtn.classList.remove('loading');
922
+ }
923
+ }
924
+ }
925
+
926
+ async function submitPreview() {
927
+ const count = currentCount();
928
+ const plan = collectPlan();
929
+ const path = '/api/tables/' + t + '/seed/generate';
930
+ try {
931
+ if (previewBtn) previewBtn.disabled = true;
932
+ const data = await Api.post(path, { count, plan });
933
+ renderPreview(data);
934
+ showFeedback(data.warnings || []);
935
+ } catch (e) {
936
+ if (window.UI && UI.showError) UI.showError(e.message);
937
+ } finally {
938
+ if (previewBtn) previewBtn.disabled = false;
345
939
  }
346
940
  }
347
941
 
@@ -352,20 +946,139 @@
352
946
  buildCard(col);
353
947
  resetCard(col);
354
948
  });
355
- if (insertBtn) insertBtn.addEventListener('click', () => submit(true));
356
- if (previewBtn) previewBtn.addEventListener('click', () => submit(false));
949
+
950
+ filterColumns();
951
+
952
+ // Event Listeners
953
+ if (searchInput) searchInput.addEventListener('input', filterColumns);
954
+ if (previewBtn) previewBtn.addEventListener('click', submitPreview);
955
+
956
+ if (insertBtn) {
957
+ insertBtn.addEventListener('click', () => {
958
+ if (truncateCheckbox && truncateCheckbox.checked) {
959
+ if (truncateDialog) truncateDialog.showModal();
960
+ } else {
961
+ executeSeed(false);
962
+ }
963
+ });
964
+ }
965
+
966
+ if (truncateConfirm) {
967
+ truncateConfirm.addEventListener('click', () => {
968
+ if (truncateDialog) truncateDialog.close();
969
+ executeSeed(true);
970
+ });
971
+ }
972
+
973
+ if (truncateCancel) {
974
+ truncateCancel.addEventListener('click', () => {
975
+ if (truncateDialog) truncateDialog.close();
976
+ });
977
+ }
978
+
357
979
  if (resetBtn) resetBtn.addEventListener('click', () => columns.forEach(resetCard));
358
- if (applyAllBtn) applyAllBtn.addEventListener('click', () => {
359
- const dialog = document.getElementById('seed-apply-all-dialog') || buildApplyAllDialog();
360
- dialog.showModal();
980
+ if (refreshSamplesBtn) refreshSamplesBtn.addEventListener('click', () => {
981
+ columns.forEach((col) => {
982
+ const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
983
+ if (card) updateCardSample(col, card);
984
+ });
361
985
  });
362
- if (copyBtn) {
363
- copyBtn.addEventListener('click', () => {
986
+
987
+ if (applyAllBtn) {
988
+ applyAllBtn.addEventListener('click', () => {
989
+ const dialog = document.getElementById('seed-apply-all-dialog') || buildApplyAllDialog();
990
+ dialog.showModal();
991
+ });
992
+ }
993
+
994
+ // Presets
995
+ const pReal = document.getElementById('preset-realistic');
996
+ const pMin = document.getElementById('preset-minimal');
997
+ const pSparse = document.getElementById('preset-sparse');
998
+ const pNum = document.getElementById('preset-numbered');
999
+ if (pReal) pReal.addEventListener('click', () => applyPreset('realistic'));
1000
+ if (pMin) pMin.addEventListener('click', () => applyPreset('minimal'));
1001
+ if (pSparse) pSparse.addEventListener('click', () => applyPreset('sparse'));
1002
+ if (pNum) pNum.addEventListener('click', () => applyPreset('numbered'));
1003
+
1004
+ // Config export / import
1005
+ if (exportConfigBtn) {
1006
+ exportConfigBtn.addEventListener('click', () => {
1007
+ if (!configDialog || !configTextarea) return;
1008
+ configDialogTitle.textContent = 'Export Seed Plan';
1009
+ configTextarea.value = JSON.stringify(collectPlan(), null, 2);
1010
+ configDialogApply.classList.add('hidden');
1011
+ configDialog.showModal();
1012
+ });
1013
+ }
1014
+
1015
+ if (importConfigBtn) {
1016
+ importConfigBtn.addEventListener('click', () => {
1017
+ if (!configDialog || !configTextarea) return;
1018
+ configDialogTitle.textContent = 'Import Seed Plan';
1019
+ configTextarea.value = '';
1020
+ configTextarea.placeholder = 'Paste plan JSON here...';
1021
+ configDialogApply.classList.remove('hidden');
1022
+ configDialog.showModal();
1023
+ });
1024
+ }
1025
+
1026
+ if (configDialogCancel) {
1027
+ configDialogCancel.addEventListener('click', () => {
1028
+ if (configDialog) configDialog.close();
1029
+ });
1030
+ }
1031
+
1032
+ if (configDialogApply) {
1033
+ configDialogApply.addEventListener('click', () => {
1034
+ try {
1035
+ const imported = JSON.parse(configTextarea.value);
1036
+ if (typeof imported !== 'object' || !imported) throw new Error('Invalid JSON format');
1037
+ columns.forEach((col) => {
1038
+ if (imported[col.name]) {
1039
+ state[col.name] = imported[col.name];
1040
+ const card = columnsBox.querySelector('.app-card[data-column="' + CSS.escape(col.name) + '"]');
1041
+ if (card) {
1042
+ const select = card.querySelector('.seed-strategy');
1043
+ const options = card.querySelector('.seed-options');
1044
+ const note = card.querySelector('.seed-note');
1045
+ const warn = card.querySelector('.seed-warn');
1046
+ if (select) select.value = state[col.name].strategy;
1047
+ buildOptionInputs(options, col, state[col.name], card);
1048
+ note.textContent = strategyNote(col, state[col.name].strategy);
1049
+ note.classList.toggle('hidden', !note.textContent);
1050
+ updateWarn(col, warn);
1051
+ updateCardSample(col, card);
1052
+ }
1053
+ }
1054
+ });
1055
+ if (configDialog) configDialog.close();
1056
+ if (window.UI && UI.showToast) UI.showToast('Seed plan configuration applied!', 'success');
1057
+ } catch (err) {
1058
+ if (window.UI && UI.showError) UI.showError('Failed to parse JSON: ' + err.message);
1059
+ }
1060
+ });
1061
+ }
1062
+
1063
+ // Dual-mode tabs
1064
+ if (tabTableView) tabTableView.addEventListener('click', () => setPreviewTab('table'));
1065
+ if (tabSqlView) tabSqlView.addEventListener('click', () => setPreviewTab('sql'));
1066
+
1067
+ // Export buttons
1068
+ if (copySqlBtn) {
1069
+ copySqlBtn.addEventListener('click', () => {
1070
+ if (!sqlCode || !sqlCode.textContent) return;
364
1071
  navigator.clipboard.writeText(sqlCode.textContent)
365
- .then(() => UI.showToast('Copied to clipboard.', 'success'))
366
- .catch(() => UI.showError('Could not copy.'));
1072
+ .then(() => UI.showToast('Copied SQL to clipboard.', 'success'))
1073
+ .catch(() => UI.showError('Could not copy SQL.'));
367
1074
  });
368
1075
  }
1076
+
1077
+ if (downloadSqlBtn) downloadSqlBtn.addEventListener('click', downloadSql);
1078
+ if (exportCsvBtn) exportCsvBtn.addEventListener('click', exportCsv);
1079
+ if (exportJsonBtn) exportJsonBtn.addEventListener('click', exportJson);
1080
+
1081
+ // Quick counts
369
1082
  document.querySelectorAll('.seed-quick').forEach((btn) => {
370
1083
  btn.addEventListener('click', () => {
371
1084
  if (countInput) countInput.value = btn.dataset.n;