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,1031 +0,0 @@
1
- "use strict";
2
- /**
3
- * Data generator / seeder engine.
4
- *
5
- * Given a table's schema, every column can be filled with realistic fake data
6
- * from a chosen "strategy" — first name, email, UUID, a random integer, a value
7
- * sampled from a referenced table's foreign key, a fixed value, and so on.
8
- *
9
- * Responsibilities:
10
- * - auto-detect a sensible default strategy per column (column name + type +
11
- * foreign key), so the "Seed data" page works with zero configuration;
12
- * - generate N rows of concrete, type-coerced values (ready for SQLite
13
- * binding via `db.insertRows`);
14
- * - render the generated rows as a multi-row INSERT for the "preview SQL"
15
- * mode that never executes.
16
- */
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.MAX_SEED_ROWS = void 0;
19
- exports.buildColumnConfigs = buildColumnConfigs;
20
- exports.sanitizeColumnPlan = sanitizeColumnPlan;
21
- exports.generateRows = generateRows;
22
- exports.buildSeedInsertSql = buildSeedInsertSql;
23
- const node_crypto_1 = require("node:crypto");
24
- const util_1 = require("../util");
25
- const generator_1 = require("../sql/generator");
26
- /** Hard cap on how many rows a single seed run may generate. */
27
- exports.MAX_SEED_ROWS = 5000;
28
- /** Sentinel returned by value generation when the column should be omitted from the row. */
29
- const SKIP = Symbol('skip');
30
- // ---- Static data sets ----------------------------------------------------
31
- const FIRST_NAMES = [
32
- "Sombat", "Ting", "Rajan", "Minh", "Hiroshi", "William", "Made", "Ravi", "John", "Kiran",
33
- "Raj", "Wayan", "Chaiwat", "Aarav", "Sita", "Zara", "Ratna", "Ananya", "Ha-eun", "Siti",
34
- "Seo-yeon", "Mary", "Priyanka", "Xu", "Ali", "Priya", "Sakura", "Ken", "Ketut", "Soo-hyun",
35
- "Wang", "Guido", "Deepa", "Fang", "James", "Mei", "Binh", "Takeshi", "Leela", "Thuy",
36
- "Dewi", "Layla", "Vijay", "Samir", "Meera", "Jessica", "Linda", "Malee", "Qiang", "Yuna",
37
- "Richard", "Margaret", "Ming", "Vikram", "Mai", "Rahul", "Van", "Jennifer", "Suresh", "Yui",
38
- "Pan", "Barbara", "Tim", "Lu", "Liang", "Robert", "Fatima", "Imran", "Ajay", "Ren",
39
- "Liu", "Dennis", "Lin", "Kavya", "Agus", "Katherine", "Anita", "Grace", "Linus", "Jun",
40
- "Alan", "Tarun", "Omar", "Michael", "Dung", "Ahmad", "Hoa", "Hui", "Susan", "Shinji",
41
- "Zhou", "Linh", "Ngoc", "Xin", "Hassan", "Nisha", "Patricia", "Li", "Lei", "Sunita",
42
- "Chen", "Anh", "Jia", "Xiao", "Yang", "Ji-hoon", "Jing", "Hedy", "Elizabeth", "Zhang",
43
- "Arjun", "Yuki", "Ada", "Edsger", "Nong", "Lynn", "Anjali", "Nguyen", "Sora", "Mieko",
44
- "Nyoman", "Jin", "Aisha", "Bayu", "Tariq", "Daiki", "David", "Rani", "Tran", "Haruki",
45
- "Dae-hyun", "Dao", "Rama", "Aiko", "Min-jun", "Zhu", "Radia", "Wei"
46
- ];
47
- const LAST_NAMES = [
48
- "Chen", "Smith", "Thompson", "Shah", "Thai", "Perlman", "Brown", "Hsu", "Turing", "Sharma",
49
- "Purnama", "Lopez", "Williams", "Davis", "Liao", "Xu", "Martin", "Dhawan", "Yoon", "Tan",
50
- "Yamamoto", "Iyer", "Phung", "Gonzalez", "Huang", "Hu", "Vo", "Shetty", "Le", "Wijaya",
51
- "Jackson", "Ly", "Anderson", "Pratama", "Lovelace", "Martinez", "Dang", "Ma", "Hopper", "Naidu",
52
- "Goh", "Hamilton", "Duong", "Taylor", "Song", "Surya", "Singh", "Jiang", "Cho", "Zhang",
53
- "Pham", "Teo", "van Rossum", "Patel", "Lam", "Jang", "Rodriguez", "Miller", "Torvalds", "Hidayat",
54
- "Menon", "Thomas", "Hernandez", "Reddy", "Wong", "Tripathi", "Lim", "Kumar", "Bhaskar", "Rajan",
55
- "Chang", "Ong", "Vu", "Nair", "Sun", "Do", "Bhatia", "Garcia", "Mishra", "Rao",
56
- "Choi", "Phan", "Zhu", "Nguyen", "Kim", "Putra", "Malhotra", "Nakamura", "Kang", "Wilson",
57
- "Huynh", "Tanaka", "Kusuma", "Jung", "Mehta", "Perez", "Arora", "Chu", "Moore", "Li",
58
- "Yang", "Park", "Ko", "Watanabe", "Bui", "Liu", "Ninh", "Jones", "Johnson", "Guo",
59
- "Ngo", "Ito", "Takahashi", "Grover", "Shen", "Yeh", "Lee", "Gupta", "Wang", "Wu",
60
- "Verma", "Suzuki", "White", "Lu", "Pillai", "Lin", "Joshi", "Tran", "Chawla", "Zhou", "Desai"
61
- ];
62
- const CITIES = [
63
- "Hyderabad", "Yangon", "Medan", "Minneapolis", "Faisalabad", "Yokohama", "Nha Trang",
64
- "Hong Kong", "Phoenix", "Delhi", "Chennai", "Denver", "Guangzhou", "Davao City",
65
- "Chiang Mai", "Atlanta", "Manila", "Naypyidaw", "Sylhet", "Phuket", "Incheon",
66
- "New York", "Daejeon", "Yogyakarta", "Colombo", "Kanpur", "Portland", "Vientiane",
67
- "Dublin", "Mandalay", "Hat Yai", "Chengdu", "Lisbon", "Paris", "Bandung",
68
- "Kathmandu", "Karachi", "Qingdao", "Da Nang", "Seoul", "Hanoi", "Berlin",
69
- "Chicago", "Sydney", "Quezon City", "Mumbai", "Vizag", "Coimbatore", "Lahore",
70
- "Dhaka", "Shenzhen", "Phnom Penh", "Navi Mumbai", "Fuzhou", "Kuala Lumpur", "Taipei",
71
- "Zurich", "Bangkok", "Makati", "Chittagong", "Daegu", "Jaipur", "Oslo",
72
- "Islamabad", "Dalian", "Kolkata", "Penang", "Johor Bahru", "Singapore", "Ulsan",
73
- "Macau", "Pune", "Pattaya", "Siem Reap", "Cebu City", "Lucknow", "Rome",
74
- "Surabaya", "Stockholm", "Los Angeles", "Osaka", "Sapporo", "Fukuoka", "Rajshahi",
75
- "Tokyo", "Vancouver", "San Francisco", "Nagoya", "Busan", "Jakarta", "Guangxi",
76
- "London", "Kaohsiung", "Xiamen", "Male", "Nashville", "Warsaw", "Madrid",
77
- "Indore", "Boston", "Amsterdam", "Shanghai", "Taichung", "Raleigh", "Miami",
78
- "Jeju", "Cheongju", "Bangalore", "Ahmedabad", "Haiphong", "Ho Chi Minh City", "Gwangju",
79
- "Rawalpindi", "Toronto", "Seville", "Kyoto", "Austin", "Suwon", "Seattle",
80
- "Beijing", "Nakhon Ratchasima", "Nagpur"
81
- ];
82
- const COUNTRIES = [
83
- "Japan", "United States", "Bhutan", "Belgium", "Myanmar", "Kazakhstan", "Mexico",
84
- "Bangladesh", "United Kingdom", "South Africa", "Indonesia", "Vietnam", "South Korea", "Germany",
85
- "Romania", "Italy", "Spain", "China", "India", "Taiwan", "Philippines",
86
- "Singapore", "Canada", "Pakistan", "Hungary", "Maldives", "Switzerland", "Macau",
87
- "Portugal", "Sri Lanka", "Malaysia", "Netherlands", "Turkmenistan", "Brunei", "Brazil",
88
- "Greece", "Uzbekistan", "Cambodia", "France", "Australia", "Mongolia", "Poland",
89
- "Kyrgyzstan", "Tajikistan", "Norway", "Sweden", "Ireland", "Austria", "Denmark",
90
- "Laos", "Nepal", "Thailand", "Finland", "Hong Kong", "Czech Republic", "Timor-Leste", "New Zealand"
91
- ];
92
- const DOMAINS = [
93
- "outlook.com", "nus.edu.sg", "ust.hk", "iitb.ac.in", "acme.io", "ac.sg", "mail.ru", "163.com",
94
- "outlook.kr", "tech.asia", "startup.io", "proton.me", "sohu.com", "gmail.com", "kyoto-u.ac.jp",
95
- "example.com", "naver.com", "protonmail.com", "hanmail.net", "globex.org", "tudelft.nl", "company.com",
96
- "yahoo.com", "126.com", "daum.net", "initech.net", "hkbu.edu.hk", "yahoo.co.jp", "qq.com", "nthu.edu.tw",
97
- "sina.com", "innovate.sg", "zoho.com"
98
- ];
99
- const LOREM_WORDS = [
100
- "schema", "nu", "worker", "alpha", "cache", "edge", "async", "chi", "service", "packet",
101
- "scale", "upsilon", "scrum", "node", "peace", "deploy", "epsilon", "lambda", "record", "pi",
102
- "column", "module", "omicron", "query", "buffer", "persistence", "comet", "joy", "integrity", "galaxy",
103
- "token", "spirit", "socket", "devops", "harmony", "backup", "agile", "render", "await", "field",
104
- "aurora", "nature", "gamma", "cloud", "omega", "mu", "orbit", "delta", "kindness", "tau",
105
- "row", "compassion", "theta", "beta", "balance", "wisdom", "rho", "kanban", "kappa", "session",
106
- "compile", "phi", "stream", "queue", "event", "pixel", "resilience", "nebula", "iota", "index",
107
- "cluster", "xi", "eta", "signal", "zeta", "fetch", "sigma", "psi"
108
- ];
109
- // ---- Random helpers -------------------------------------------------------
110
- function randInt(min, max) {
111
- return Math.floor(Math.random() * (max - min + 1)) + min;
112
- }
113
- function randFloat(min, max) {
114
- return Math.random() * (max - min) + min;
115
- }
116
- function pick(arr) {
117
- return arr[randInt(0, arr.length - 1)];
118
- }
119
- function slug() {
120
- return `${pick(LOREM_WORDS)}-${pick(LOREM_WORDS)}-${randInt(100, 999)}`;
121
- }
122
- function makeEmail() {
123
- return `${pick(FIRST_NAMES).toLowerCase()}.${pick(LAST_NAMES).toLowerCase()}${randInt(1, 99)}@${pick(DOMAINS)}`;
124
- }
125
- function makeUsername() {
126
- return `${pick(FIRST_NAMES).toLowerCase()}${pick(LAST_NAMES).toLowerCase()}${randInt(1, 9999)}`;
127
- }
128
- function makePhone() {
129
- return `+1 (${randInt(200, 999)}) ${randInt(200, 999)}-${String(randInt(0, 9999)).padStart(4, '0')}`;
130
- }
131
- function makeAddress() {
132
- return `${randInt(10, 9999)} ${pick(LOREM_WORDS)} ${pick(['St', 'Ave', 'Rd', 'Blvd', 'Ln', 'Dr'])}, ${pick(CITIES)}`;
133
- }
134
- function makeIp() {
135
- return `${randInt(1, 223)}.${randInt(0, 255)}.${randInt(0, 255)}.${randInt(1, 254)}`;
136
- }
137
- function makeUuid() {
138
- const hex = () => randInt(0, 0xffffffff).toString(16).padStart(8, '0');
139
- return `${hex()}-${hex().slice(0, 4)}-4${hex().slice(0, 3)}-${['8', '9', 'a', 'b'][randInt(0, 3)]}${hex().slice(0, 3)}-${hex()}`;
140
- }
141
- function makeWords(minLen, maxLen) {
142
- const n = randInt(minLen, maxLen);
143
- const parts = [];
144
- for (let i = 0; i < n; i++)
145
- parts.push(pick(LOREM_WORDS));
146
- return parts.join(' ');
147
- }
148
- function makeSentences(minLen, maxLen) {
149
- const n = randInt(minLen, maxLen);
150
- const sentences = [];
151
- for (let i = 0; i < n; i++) {
152
- const words = makeWords(6, 14).split(' ');
153
- words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
154
- sentences.push(words.join(' ') + '.');
155
- }
156
- return sentences.join(' ');
157
- }
158
- /**
159
- * Random date (or datetime) string inside [from, to]; bounds default to the
160
- * last ~2 years. Datetimes use the ISO-8601 UTC form (YYYY-MM-DDTHH:MM:SS.sssZ)
161
- * so TEXT timestamp columns — e.g. a strftime('%Y-%m-%dT%H:%M:%fZ','now')
162
- * default — get values consistent with their existing data.
163
- */
164
- function randomDate(from, to, kind = 'date') {
165
- const now = Date.now();
166
- const lo = from ? Date.parse(from) : now - 730 * 86400_000;
167
- const hi = to ? Date.parse(to) : now;
168
- const t = randInt(Number.isFinite(lo) ? lo : now - 730 * 86400_000, Number.isFinite(hi) ? hi : now);
169
- const d = new Date(t);
170
- if (kind === 'datetime')
171
- return d.toISOString();
172
- const p = (n, w = 2) => String(n).padStart(w, '0');
173
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
174
- }
175
- function randomTime() {
176
- return `${String(randInt(0, 23)).padStart(2, '0')}:${String(randInt(0, 59)).padStart(2, '0')}:${String(randInt(0, 59)).padStart(2, '0')}`;
177
- }
178
- // ---- Column name / type detection ----------------------------------------
179
- /** Lowercase, punctuation stripped — e.g. "first_name" → "firstname". */
180
- function normName(s) {
181
- return String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
182
- }
183
- /**
184
- * True when a (normalized, lowercase) column name looks like a timestamp /
185
- * datetime field — created_at, CreatedAt, UpdatedAt, DeletedAt, OccurredAt,
186
- * expires_at, LastUsed, modified, joined, ts, … (camelCase is normalized to one
187
- * lowercase token, so both "CreatedAt" and "created_at" match).
188
- */
189
- function isTimestampName(name) {
190
- return (/created|updated|modified|registered|joined|timestamp|lastused|lastseen|lastlogin|lastactivity|accessed|viewed|visited|^ts$/.test(name) ||
191
- /^(created|updated|deleted|archived|occurred|started|ended|expires|published|processed|completed|inserted|requested|resolved|used|seen|accessed|viewed|visited|logged|active)at$/.test(name));
192
- }
193
- /** True when a declared type can reasonably hold text-ish values (incl. JSON). */
194
- function isTextishType(type) {
195
- const t = (type || '').toUpperCase();
196
- return t === 'TEXT' || t === '' || t.includes('JSON') || t.includes('CHAR') || t.includes('CLOB');
197
- }
198
- /** Extract balanced `CHECK(...)` bodies from a CREATE TABLE statement. */
199
- function extractCheckExprs(sql) {
200
- const out = [];
201
- const re = /CHECK\s*\(/gi;
202
- let m;
203
- while ((m = re.exec(sql))) {
204
- let depth = 1;
205
- let i = m.index + m[0].length;
206
- const start = i;
207
- while (i < sql.length && depth > 0) {
208
- const ch = sql[i];
209
- if (ch === "'") {
210
- i += 1;
211
- while (i < sql.length) {
212
- if (sql[i] === "'") {
213
- if (sql[i + 1] === "'")
214
- i += 2;
215
- else {
216
- i += 1;
217
- break;
218
- }
219
- }
220
- else
221
- i += 1;
222
- }
223
- continue;
224
- }
225
- if (ch === '"') {
226
- i += 1;
227
- while (i < sql.length && sql[i] !== '"')
228
- i += 1;
229
- i += 1;
230
- continue;
231
- }
232
- if (ch === '(')
233
- depth += 1;
234
- else if (ch === ')') {
235
- depth -= 1;
236
- if (depth === 0)
237
- break;
238
- }
239
- i += 1;
240
- }
241
- out.push(sql.slice(start, i));
242
- }
243
- return out;
244
- }
245
- /**
246
- * Parse a table's CHECK constraints for two common, seed-relevant shapes:
247
- * - JSON value whitelists: `("Permissions" ->> 'scope') IN ('read-only','write')`
248
- * - column ordering: `("ExpiresAt" > "CreatedAt")`
249
- */
250
- function parseChecks(sql) {
251
- const info = { jsonAllowed: new Map(), ordering: new Map() };
252
- if (!sql)
253
- return info;
254
- const setAfter = (col, other, orEqual) => {
255
- const c = info.ordering.get(col) ?? {};
256
- c.after = { column: other, orEqual };
257
- info.ordering.set(col, c);
258
- };
259
- const setBefore = (col, other, orEqual) => {
260
- const c = info.ordering.get(col) ?? {};
261
- c.before = { column: other, orEqual };
262
- info.ordering.set(col, c);
263
- };
264
- for (const expr of extractCheckExprs(sql)) {
265
- // JSON whitelists: ("col" ->> 'key') IN (...) / json_extract("col", '$.key') IN (...)
266
- // (the inner group wraps `('key')`, so allow optional `)` before IN)
267
- const jw = /"([^"]+)"\s*(?:->>|->)\s*'([^']+)'\s*\)*\s*IN\s*\(([^)]*)\)/g;
268
- const jw2 = /json_extract\s*\(\s*"([^"]+)"\s*,\s*'(?:\$\??|\.)?\.?([^']+)'\s*\)\s*\)*\s*IN\s*\(([^)]*)\)/g;
269
- for (const re of [jw, jw2]) {
270
- let m;
271
- while ((m = re.exec(expr))) {
272
- const values = (m[3].match(/'([^']*)'/g) ?? []).map((s) => s.replace(/^'|'$/g, ''));
273
- if (!values.length)
274
- continue;
275
- const byCol = info.jsonAllowed.get(m[1]) ?? new Map();
276
- byCol.set(m[2], values);
277
- info.jsonAllowed.set(m[1], byCol);
278
- }
279
- }
280
- // Column ordering: "A" > "B", "A" >= "B", "B" < "A", …
281
- const ord = /"([^"]+)"\s*(>=|<=|>|<)\s*"([^"]+)"/g;
282
- let m2;
283
- while ((m2 = ord.exec(expr))) {
284
- const a = m2[1];
285
- const op = m2[2];
286
- const b = m2[3];
287
- if (op === '>' || op === '>=') {
288
- setAfter(a, b, op === '>=');
289
- setBefore(b, a, op === '>=');
290
- }
291
- else {
292
- setBefore(a, b, op === '<=');
293
- setAfter(b, a, op === '<=');
294
- }
295
- }
296
- }
297
- return info;
298
- }
299
- /** True when a (normalized) column name suggests a hash / secret / token value. */
300
- function isHashName(name) {
301
- return (/^(hash|digest|secret|token|passphrase)$/.test(name) ||
302
- /tokenhash|passwordhash|passhash|accesskey|apikey|api_key|secretkey|clientsecret|appsecret|appid|privatekey|publickey/.test(name));
303
- }
304
- /** True when a (normalized) column name suggests a short opaque token / code. */
305
- function isTokenName(name) {
306
- return (/^prefix$/.test(name) ||
307
- /tokenprefix/.test(name) ||
308
- /(code|ref|reference|sku|ticketno|ticketnum|orderno|order_no|trackingno|trackingnum|invno|invoiceno|serialno)$/.test(name));
309
- }
310
- /** Parse a column's DEFAULT expression; when it is a JSON value, return it. */
311
- function jsonDefault(dflt) {
312
- if (!dflt)
313
- return null;
314
- let s = String(dflt).trim();
315
- // Unwrap a single-quoted SQL string literal, e.g. '{"scope":"read-only"}'.
316
- const m = s.match(/^'((?:[^']|'')*)'$/);
317
- if (m)
318
- s = m[1].replace(/''/g, "'");
319
- try {
320
- const v = JSON.parse(s);
321
- return v && typeof v === 'object' ? v : null;
322
- }
323
- catch {
324
- return null;
325
- }
326
- }
327
- /**
328
- * True when a TEXT-ish column must hold valid JSON: it is referenced by a
329
- * JSON-expression index (e.g. `Permissions ->> 'scope'`), its default is JSON,
330
- * or its name strongly implies a JSON document. Filling such a column with
331
- * random words throws "malformed JSON" at insert time.
332
- */
333
- function isJsonColumn(col, hasJsonIndex) {
334
- if (!isTextishType(col.type))
335
- return false;
336
- if (hasJsonIndex)
337
- return true;
338
- if (jsonDefault(col.dflt_value) != null)
339
- return true;
340
- const name = normName(col.name);
341
- return (/^(permissions|settings|config|configuration|preferences|prefs|metadata|meta|attributes|payload|options|allowedapps|socialaccounts|positions|fcmtokens|fcm_tokens|scopes|profile|json|jsonb)$/.test(name) ||
342
- /(^|_)(settings|config|configuration|permissions|prefs|metadata|attributes|payload|options|allowedapps|scopes|profile)$/.test(name));
343
- }
344
- /**
345
- * Columns + keys referenced by JSON operators in an index statement
346
- * (`->`, `->>`, `json_extract`). PRAGMA index_info reports expression-index
347
- * columns as null, so the SQL text is the reliable source.
348
- */
349
- function extractJsonColumns(sql) {
350
- const out = [];
351
- const push = (col, key) => {
352
- let entry = out.find((e) => e.col === col);
353
- if (!entry) {
354
- entry = { col, keys: [] };
355
- out.push(entry);
356
- }
357
- if (key) {
358
- const last = key.split('.').pop().replace(/[\[\]"]/g, '');
359
- if (last && !entry.keys.includes(last))
360
- entry.keys.push(last);
361
- }
362
- };
363
- // "col" ->> 'key' | col ->> 'key' | -> 'key'
364
- const arrow = /"([^"]+)"\s*(?:->>|->)\s*'([^']+)'|([A-Za-z_][A-Za-z0-9_]*)\s*(?:->>|->)\s*'([^']+)'/g;
365
- let m;
366
- while ((m = arrow.exec(sql)))
367
- push(m[1] || m[3], m[2] || m[4]);
368
- // json_extract("col", '$.key') | json_extract(col, '$.key')
369
- const jx = /json_extract\s*\(\s*(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*,\s*'(?:\$\??|\.)?\.?([^']+)'/g;
370
- while ((m = jx.exec(sql)))
371
- push(m[1] || m[2], m[3]);
372
- return out;
373
- }
374
- /**
375
- * Pick a sensible default plan for a column. Order matters: foreign keys and
376
- * primary keys first, then explicit types, then name-based heuristics for TEXT.
377
- */
378
- function detectPlan(col, fk) {
379
- const type = (col.type || '').toUpperCase();
380
- const rawName = col.name.toLowerCase();
381
- const name = normName(col.name);
382
- if (fk)
383
- return { strategy: 'fk' };
384
- if (col.pk) {
385
- if (type.startsWith('INTEGER'))
386
- return { strategy: 'skip' }; // autoincrement / rowid
387
- if (type === 'TEXT' || type === 'BLOB')
388
- return { strategy: 'uuid' };
389
- return { strategy: 'skip' };
390
- }
391
- if (type === 'BOOLEAN')
392
- return { strategy: 'bool' };
393
- // Integer flags (is_active, has_badge, …) are booleans in disguise.
394
- if (type.startsWith('INTEGER') &&
395
- /(^|_)(is|has|can|should|did)_|_flag$|active|enabled|verified|published|deleted|archived|paid|completed|approved/.test(rawName)) {
396
- return { strategy: 'bool' };
397
- }
398
- if (type === 'DATE' || /^date|birthdate|dob|birthday/.test(rawName)) {
399
- return name.includes('birth')
400
- ? { strategy: 'date', from: '1950-01-01', to: '2000-12-31' }
401
- : { strategy: 'date', from: '2020-01-01', to: '2026-12-31' };
402
- }
403
- if (type === 'DATETIME' || type === 'TIMESTAMP' || isTimestampName(name)) {
404
- return { strategy: 'datetime', from: '2024-01-01T00:00:00', to: '2026-12-31T23:59:59' };
405
- }
406
- if (type === 'TIME' || name === 'time')
407
- return { strategy: 'time' };
408
- if (type.startsWith('INTEGER')) {
409
- if (name.includes('year'))
410
- return { strategy: 'int', min: 1990, max: 2026 };
411
- if (name.includes('age'))
412
- return { strategy: 'int', min: 18, max: 90 };
413
- if (/(count|quantity|qty|stock|views|likes|total|num|number)$/.test(name))
414
- return { strategy: 'int', min: 0, max: 100000 };
415
- if (name.includes('rating') || name.includes('score'))
416
- return { strategy: 'int', min: 1, max: 5 };
417
- return { strategy: 'int', min: 1, max: 1000 };
418
- }
419
- if (type === 'REAL') {
420
- if (/(price|cost|amount|total|salary|balance)/.test(name))
421
- return { strategy: 'decimal', min: 1, max: 5000, precision: 2 };
422
- if (name.includes('rating'))
423
- return { strategy: 'decimal', min: 1, max: 5, precision: 1 };
424
- return { strategy: 'decimal', min: 1, max: 1000, precision: 2 };
425
- }
426
- if (type === 'BLOB')
427
- return { strategy: 'bytes', minLen: 8, maxLen: 32 };
428
- // TEXT / untyped → name-based heuristics.
429
- if (isHashName(name))
430
- return { strategy: 'hash', minLen: 32, maxLen: 64 };
431
- if (isTokenName(name))
432
- return { strategy: 'token', minLen: 6, maxLen: 12 };
433
- if (/uuid|guid/.test(name))
434
- return { strategy: 'uuid' };
435
- if (name === 'id' || name === 'uid' || name === 'key')
436
- return { strategy: 'uuid' };
437
- if (name.includes('first') && name.includes('name'))
438
- return { strategy: 'first' };
439
- if (/(lastname|surname|familyname)/.test(name) || (name.includes('last') && name.includes('name')))
440
- return { strategy: 'last' };
441
- if (name === 'name' || name === 'fullname' || (name.includes('full') && name.includes('name')))
442
- return { strategy: 'fullname' };
443
- if (name.includes('email') || name.includes('mail'))
444
- return { strategy: 'email' };
445
- if (/(username|login|handle|nick|nickname|account|user)/.test(name))
446
- return { strategy: 'username' };
447
- if (/(phone|mobile|telephone|cell|fax)/.test(name))
448
- return { strategy: 'phone' };
449
- if (name.includes('city') || name.includes('town'))
450
- return { strategy: 'city' };
451
- if (name.includes('country') || name.includes('nation'))
452
- return { strategy: 'country' };
453
- if (/(postal|zip)/.test(name))
454
- return { strategy: 'postal' };
455
- if (/(address|street|addr)/.test(name))
456
- return { strategy: 'address' };
457
- if (/(url|website|site|link|homepage|href)/.test(name))
458
- return { strategy: 'url' };
459
- if (/(domain|host)/.test(name))
460
- return { strategy: 'domain' };
461
- if (name === 'ip' || /(ipaddr|ipv4|ip_address)/.test(name))
462
- return { strategy: 'ip' };
463
- if (/(description|content|body|comment|message|note|bio|summary|details)/.test(name))
464
- return { strategy: 'sentence', minLen: 1, maxLen: 2 };
465
- if (/(title|subject|category|status|type|tag|label|color|role)/.test(name))
466
- return { strategy: 'words', minLen: 1, maxLen: 3 };
467
- return { strategy: 'words', minLen: 1, maxLen: 4 };
468
- }
469
- /** The strategies a column may use, in UI order. */
470
- function strategiesFor(col, fk) {
471
- const type = (col.type || '').toUpperCase();
472
- const rawName = col.name.toLowerCase();
473
- const name = normName(col.name);
474
- const out = [];
475
- const add = (id, label) => out.push({ id, label });
476
- add('skip', 'Skip — let the DB default apply');
477
- add('null', 'Set NULL');
478
- add('fixed', 'Fixed value');
479
- add('list', 'Random from list');
480
- if (fk)
481
- add('fk', `Random from ${fk.table}${fk.to ? `.${fk.to}` : ''}`);
482
- if (type === 'BOOLEAN' || (type.startsWith('INTEGER') && /flag|active|enabled|status/.test(col.name.toLowerCase()))) {
483
- add('bool', 'Random boolean');
484
- }
485
- if (type.startsWith('INTEGER'))
486
- add('int', 'Random integer');
487
- if (type === 'REAL')
488
- add('decimal', 'Random decimal');
489
- if (type === 'DATE')
490
- add('date', 'Random date');
491
- if (type === 'DATETIME' || type === 'TIMESTAMP')
492
- add('datetime', 'Random datetime');
493
- if (type === 'TIME')
494
- add('time', 'Random time');
495
- if (type === 'BLOB')
496
- add('bytes', 'Random bytes');
497
- if (isTextishType(type)) {
498
- // Date-ish TEXT columns usually hold ISO timestamps (e.g. a
499
- // strftime('%Y-%m-%dT%H:%M:%fZ','now') default). Offer the matching
500
- // generators so a detected date/datetime/time default stays valid for them.
501
- if (/^date|birthdate|dob|birthday/.test(rawName))
502
- add('date', 'Random date');
503
- if (isTimestampName(name))
504
- add('datetime', 'Random datetime');
505
- if (name === 'time')
506
- add('time', 'Random time');
507
- add('json', 'Valid JSON object');
508
- add('hash', 'Random hash (hex)');
509
- add('token', 'Random token');
510
- add('words', 'A few words');
511
- add('sentence', 'Sentence(s)');
512
- add('first', 'First name');
513
- add('last', 'Last name');
514
- add('fullname', 'Full name');
515
- add('email', 'Email');
516
- add('username', 'Username');
517
- add('phone', 'Phone number');
518
- add('city', 'City');
519
- add('country', 'Country');
520
- add('postal', 'Postal code');
521
- add('address', 'Street address');
522
- add('url', 'URL');
523
- add('domain', 'Domain');
524
- add('ip', 'IP address');
525
- add('uuid', 'UUID');
526
- }
527
- return out;
528
- }
529
- /**
530
- * A safe, value-generating default plan for a column's declared type. Used as a
531
- * fallback whenever the name/type heuristic would pick a strategy the column
532
- * does not actually support, or a NOT NULL column cannot be skipped.
533
- */
534
- function fallbackPlanFor(col) {
535
- const type = (col.type || '').toUpperCase();
536
- if (type === 'BOOLEAN')
537
- return { strategy: 'bool' };
538
- if (type.startsWith('INTEGER'))
539
- return { strategy: 'int', min: 1, max: 1000 };
540
- if (type === 'REAL')
541
- return { strategy: 'decimal', min: 1, max: 1000, precision: 2 };
542
- if (type === 'DATE')
543
- return { strategy: 'date', from: '2020-01-01', to: '2026-12-31' };
544
- if (type === 'DATETIME' || type === 'TIMESTAMP')
545
- return { strategy: 'datetime', from: '2024-01-01T00:00:00', to: '2026-12-31T23:59:59' };
546
- if (type === 'TIME')
547
- return { strategy: 'time' };
548
- if (type === 'BLOB')
549
- return { strategy: 'bytes', minLen: 8, maxLen: 32 };
550
- return { strategy: 'words', minLen: 1, maxLen: 4 };
551
- }
552
- /**
553
- * Compute per-column generator configuration for a table (default strategies,
554
- * available strategies, FK metadata). Pure — no DB access — so the page route
555
- * and the API can share it.
556
- */
557
- function buildColumnConfigs(info) {
558
- const fkByColumn = new Map();
559
- for (const fk of info.foreignKeys)
560
- fkByColumn.set(fk.from, fk);
561
- // Which columns are covered by a single-column UNIQUE index (their generated
562
- // values must be unique) and which columns are referenced by a JSON-expression
563
- // index (their generated values must be valid JSON).
564
- const uniqueCols = new Set();
565
- const jsonIndexKeys = new Map();
566
- for (const ix of info.indexes ?? []) {
567
- // Single-column UNIQUE indexes (PRAGMA index_info reports real names for
568
- // regular indexes; expression indexes have name=null and are skipped).
569
- if (ix.unique && ix.columns.length === 1 && ix.columns[0])
570
- uniqueCols.add(ix.columns[0]);
571
- // JSON-expression indexes: columns must be filled with valid JSON.
572
- if (ix.sql && /json_extract|->>|->\s*'|json_/i.test(ix.sql)) {
573
- for (const { col, keys } of extractJsonColumns(ix.sql)) {
574
- if (!col)
575
- continue;
576
- const merged = new Set([...(jsonIndexKeys.get(col) ?? []), ...keys]);
577
- if (merged.size)
578
- jsonIndexKeys.set(col, [...merged]);
579
- }
580
- }
581
- }
582
- // CHECK constraints (JSON value whitelists + column ordering) the generator
583
- // can honor so generated rows pass the table's CHECKs.
584
- const checkInfo = parseChecks(info.sql ?? null);
585
- return info.columns.map((col) => {
586
- const fk = fkByColumn.get(col.name) ?? null;
587
- const strategies = strategiesFor(col, fk);
588
- const unique = uniqueCols.has(col.name);
589
- let defaultPlan = detectPlan(col, fk);
590
- // A column referenced by a JSON-expression index (or with a JSON default /
591
- // JSON-like name) must hold valid JSON — filling it with random words throws
592
- // "malformed JSON" at insert time because the index evaluates the JSON path.
593
- if (defaultPlan.strategy !== 'fk' && !col.pk && isJsonColumn(col, jsonIndexKeys.has(col.name))) {
594
- const keys = jsonIndexKeys.get(col.name);
595
- const useKeys = keys && keys.length ? keys : ['scope', 'enabled', 'allowedApps'];
596
- const allowed = checkInfo.jsonAllowed.get(col.name);
597
- const jsonValues = {};
598
- if (allowed)
599
- for (const k of useKeys) {
600
- const vals = allowed.get(k);
601
- if (vals && vals.length)
602
- jsonValues[k] = vals;
603
- }
604
- defaultPlan = {
605
- strategy: 'json',
606
- jsonKeys: useKeys,
607
- ...(Object.keys(jsonValues).length ? { jsonValues } : {}),
608
- };
609
- }
610
- // The detected default must always be a strategy the column actually
611
- // supports (e.g. TEXT "CreatedAt" detects as datetime, so datetime must be
612
- // offered to TEXT columns — see strategiesFor). Any remaining mismatch
613
- // falls back to a type-appropriate value generator so the default can never
614
- // be rejected by generateRows.
615
- if (!strategies.some((s) => s.id === defaultPlan.strategy)) {
616
- defaultPlan = fallbackPlanFor(col);
617
- }
618
- // A NOT NULL column without a default cannot fall back on skip/NULL (unless
619
- // it is a primary key, which SQLite fills for us).
620
- if (!col.pk && col.notnull && col.dflt_value == null && (defaultPlan.strategy === 'skip' || defaultPlan.strategy === 'null')) {
621
- defaultPlan = fallbackPlanFor(col);
622
- }
623
- return {
624
- name: col.name,
625
- type: col.type,
626
- pk: col.pk > 0,
627
- notnull: !!col.notnull,
628
- hasDefault: col.dflt_value != null,
629
- unique,
630
- fk,
631
- strategies,
632
- defaultPlan,
633
- };
634
- });
635
- }
636
- // ---- Plan sanitization (from untrusted client input) ----------------------
637
- const PLAN_KEYS = ['value', 'values', 'min', 'max', 'precision', 'from', 'to', 'minLen', 'maxLen', 'jsonKeys'];
638
- /**
639
- * Validate/normalize a client-supplied per-column plan. Returns null when the
640
- * payload is not a usable plan. Strategy validity against the column's allowed
641
- * set is enforced later by `generateRows`.
642
- */
643
- function sanitizeColumnPlan(raw) {
644
- if (!raw || typeof raw !== 'object')
645
- return null;
646
- const r = raw;
647
- if (typeof r.strategy !== 'string' || !r.strategy)
648
- return null;
649
- const out = { strategy: r.strategy };
650
- for (const key of PLAN_KEYS) {
651
- const v = r[key];
652
- if (v === undefined || v === null)
653
- continue;
654
- if (key === 'values') {
655
- if (Array.isArray(v))
656
- out.values = v.map((x) => String(x));
657
- }
658
- else if (key === 'jsonKeys') {
659
- const raw = Array.isArray(v) ? v.map(String) : String(v).split(/[,;]/);
660
- out.jsonKeys = raw.map((s) => s.trim()).filter(Boolean);
661
- }
662
- else if (key === 'value' || key === 'from' || key === 'to') {
663
- out[key] = String(v);
664
- }
665
- else {
666
- const n = Number(v);
667
- if (Number.isFinite(n))
668
- out[key] = n;
669
- }
670
- }
671
- // Per-key allowed JSON values (derived from CHECK constraints on the server;
672
- // passed through untouched so a generated plan keeps them).
673
- if (r.jsonValues && typeof r.jsonValues === 'object' && !Array.isArray(r.jsonValues)) {
674
- const jv = {};
675
- for (const [k, vals] of Object.entries(r.jsonValues)) {
676
- if (Array.isArray(vals)) {
677
- const cleaned = vals.map((x) => String(x).trim()).filter(Boolean);
678
- if (cleaned.length)
679
- jv[k] = cleaned;
680
- }
681
- }
682
- if (Object.keys(jv).length)
683
- out.jsonValues = jv;
684
- }
685
- return out;
686
- }
687
- // ---- Value generation -----------------------------------------------------
688
- const ALNUM = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
689
- const JSON_SCOPES = ['read-only', 'read-write', 'admin', 'user', 'internal'];
690
- const JSON_APPS = ['portal', 'api', 'dashboard', 'worker', 'admin', 'web', 'mobile'];
691
- function randomAlnum(min, max) {
692
- const len = randInt(min, max);
693
- let s = '';
694
- for (let i = 0; i < len; i++)
695
- s += ALNUM[randInt(0, ALNUM.length - 1)];
696
- return s;
697
- }
698
- /** Pick a plausible JSON value for a key (key-name aware, but always valid JSON). */
699
- function jsonValueForKey(key) {
700
- const k = key.toLowerCase();
701
- if (k.includes('scope') || k === 'role' || k === 'access')
702
- return pick(JSON_SCOPES);
703
- if (/(enabled|active|flag|locked|verified|required|readonly|is_)/.test(k))
704
- return Math.random() < 0.6;
705
- if (/(count|limit|max|min|ttl|expires|duration|port|timeout|attempts)/.test(k))
706
- return randInt(0, 10000);
707
- if (/(apps|app|tags|roles|scopes|permissions|names|ids|items|keys)/.test(k)) {
708
- if (Math.random() < 0.3)
709
- return null;
710
- const n = randInt(1, 4);
711
- const out = [];
712
- for (let i = 0; i < n; i++)
713
- out.push(pick(JSON_APPS));
714
- return out;
715
- }
716
- const roll = Math.random();
717
- if (roll < 0.45)
718
- return pick(['alpha', 'beta', 'stable', 'draft', 'on', 'off', 'default', 'custom']);
719
- if (roll < 0.6)
720
- return randInt(0, 1000);
721
- if (roll < 0.75)
722
- return Math.random() < 0.5;
723
- if (roll < 0.9)
724
- return null;
725
- return makeUuid();
726
- }
727
- /** Build a valid JSON object from a set of keys (or a small sensible default). */
728
- function makeJsonObject(keys, jsonValues) {
729
- const k = (keys && keys.length ? keys : ['scope', 'enabled', 'flags']).slice(0, 16);
730
- const obj = {};
731
- for (const key of k) {
732
- const allowed = jsonValues ? jsonValues[key] : undefined;
733
- obj[key] = allowed && allowed.length ? pick(allowed) : jsonValueForKey(key);
734
- }
735
- return JSON.stringify(obj);
736
- }
737
- /** Distinct non-null values of a foreign key's referenced column. */
738
- async function sampleFkValues(db, fk) {
739
- if (!fk)
740
- return [];
741
- try {
742
- const ref = await db.getTableInfo(fk.table);
743
- if (!ref.success || !ref.data || !ref.data.columns.length)
744
- return [];
745
- const refCol = fk.to || ref.data.primaryKey[0] || ref.data.columns[0].name;
746
- if (!refCol)
747
- return [];
748
- const r = await db.all(`SELECT DISTINCT ${(0, generator_1.quoteIdentifier)(refCol)} AS v FROM ${(0, generator_1.quoteIdentifier)(fk.table)} WHERE ${(0, generator_1.quoteIdentifier)(refCol)} IS NOT NULL LIMIT 1000`);
749
- return (r.data ?? []).map((row) => row.v).filter((v) => v != null);
750
- }
751
- catch {
752
- return [];
753
- }
754
- }
755
- /** Generate a single value for a column from its plan (or SKIP to omit it). */
756
- function generateOne(col, p, fkPool) {
757
- const type = (col.type || '').toUpperCase();
758
- switch (p.strategy) {
759
- case 'skip':
760
- return SKIP;
761
- case 'null':
762
- return null;
763
- case 'fixed':
764
- return (0, util_1.coerceFormValue)(p.value ?? '', col.type);
765
- case 'list': {
766
- const vals = (p.values ?? []).filter((v) => String(v).trim() !== '');
767
- if (!vals.length)
768
- return SKIP;
769
- return (0, util_1.coerceFormValue)(pick(vals), col.type);
770
- }
771
- case 'first':
772
- return (0, util_1.coerceFormValue)(pick(FIRST_NAMES), col.type);
773
- case 'last':
774
- return (0, util_1.coerceFormValue)(pick(LAST_NAMES), col.type);
775
- case 'fullname':
776
- return (0, util_1.coerceFormValue)(`${pick(FIRST_NAMES)} ${pick(LAST_NAMES)}`, col.type);
777
- case 'email':
778
- return (0, util_1.coerceFormValue)(makeEmail(), col.type);
779
- case 'username':
780
- return (0, util_1.coerceFormValue)(makeUsername(), col.type);
781
- case 'phone':
782
- return (0, util_1.coerceFormValue)(makePhone(), col.type);
783
- case 'city':
784
- return (0, util_1.coerceFormValue)(pick(CITIES), col.type);
785
- case 'country':
786
- return (0, util_1.coerceFormValue)(pick(COUNTRIES), col.type);
787
- case 'postal':
788
- return (0, util_1.coerceFormValue)(String(randInt(10000, 99999)), col.type);
789
- case 'address':
790
- return (0, util_1.coerceFormValue)(makeAddress(), col.type);
791
- case 'url':
792
- return (0, util_1.coerceFormValue)(`https://www.${pick(DOMAINS)}/${slug()}`, col.type);
793
- case 'domain':
794
- return (0, util_1.coerceFormValue)(pick(DOMAINS), col.type);
795
- case 'ip':
796
- return (0, util_1.coerceFormValue)(makeIp(), col.type);
797
- case 'uuid':
798
- return (0, util_1.coerceFormValue)(makeUuid(), col.type);
799
- case 'words':
800
- return (0, util_1.coerceFormValue)(makeWords(p.minLen ?? 1, p.maxLen ?? 4), col.type);
801
- case 'sentence':
802
- return (0, util_1.coerceFormValue)(makeSentences(p.minLen ?? 1, p.maxLen ?? 2), col.type);
803
- case 'int': {
804
- const min = p.min ?? 1;
805
- const max = Math.max(min, p.max ?? 1000);
806
- return (0, util_1.coerceFormValue)(String(randInt(min, max)), type || 'INTEGER');
807
- }
808
- case 'decimal': {
809
- const min = p.min ?? 1;
810
- const max = Math.max(min, p.max ?? 1000);
811
- const prec = Math.max(0, Math.min(10, p.precision ?? 2));
812
- return (0, util_1.coerceFormValue)(randFloat(min, max).toFixed(prec), type || 'REAL');
813
- }
814
- case 'date':
815
- return (0, util_1.coerceFormValue)(randomDate(p.from, p.to, 'date'), type || 'TEXT');
816
- case 'datetime':
817
- return (0, util_1.coerceFormValue)(randomDate(p.from, p.to, 'datetime'), type || 'TEXT');
818
- case 'time':
819
- return (0, util_1.coerceFormValue)(randomTime(), type || 'TEXT');
820
- case 'bool':
821
- return (0, util_1.coerceFormValue)(pick(['1', '0']), type || 'BOOLEAN');
822
- case 'bytes': {
823
- const len = randInt(Math.max(0, p.minLen ?? 8), Math.max(1, p.maxLen ?? 32));
824
- const buf = Buffer.alloc(len);
825
- for (let i = 0; i < len; i++)
826
- buf[i] = randInt(0, 255);
827
- return (0, util_1.coerceFormValue)(`0x${buf.toString('hex')}`, type || 'BLOB');
828
- }
829
- case 'json':
830
- return (0, util_1.coerceFormValue)(makeJsonObject(p.jsonKeys, p.jsonValues), type || 'TEXT');
831
- case 'hash': {
832
- const min = Math.max(8, p.minLen ?? 32);
833
- const max = Math.max(min, p.maxLen ?? 64);
834
- const len = randInt(min, max);
835
- return (0, util_1.coerceFormValue)((0, node_crypto_1.randomBytes)(Math.ceil(len / 2)).toString('hex').slice(0, len), type || 'TEXT');
836
- }
837
- case 'token':
838
- return (0, util_1.coerceFormValue)(randomAlnum(Math.max(2, p.minLen ?? 6), Math.max(2, p.maxLen ?? 12)), type || 'TEXT');
839
- case 'fk': {
840
- if (!fkPool || !fkPool.length)
841
- return null;
842
- return fkPool[randInt(0, fkPool.length - 1)];
843
- }
844
- default:
845
- return SKIP;
846
- }
847
- }
848
- /**
849
- * Generate `count` rows for a table. `plan` is a per-column override map;
850
- * columns without an entry use their detected default plan. Values are coerced
851
- * to the column's declared type for SQLite binding.
852
- *
853
- * Throws when a plan uses a strategy that the column does not support.
854
- */
855
- function stringifyKey(v) {
856
- if (v instanceof Uint8Array || Buffer.isBuffer(v))
857
- return `buf:${Buffer.from(v).toString('hex')}`;
858
- return `${typeof v}:${String(v)}`;
859
- }
860
- /** Append a unique suffix to a colliding value (numeric columns stay numeric). */
861
- function uniqueSuffix(base, seen) {
862
- if (typeof base === 'number' && Number.isFinite(base)) {
863
- let n = Number(base);
864
- while (seen.has(stringifyKey(n)))
865
- n += 1;
866
- return n;
867
- }
868
- let i = 1;
869
- let cand = `${String(base)}-${i}`;
870
- while (seen.has(stringifyKey(cand))) {
871
- i += 1;
872
- cand = `${String(base)}-${i}`;
873
- }
874
- return cand;
875
- }
876
- /**
877
- * Move `value` so it is strictly after / before `base` (or equal when
878
- * `orEqual`), for datetime/date strings and numbers. Unrecognized types are
879
- * returned unchanged.
880
- */
881
- function orderedValue(value, base, dir, orEqual) {
882
- const v = value;
883
- const b = base;
884
- if (typeof v === 'string' && typeof b === 'string') {
885
- const tv = Date.parse(v);
886
- const tb = Date.parse(b);
887
- if (Number.isFinite(tv) && Number.isFinite(tb)) {
888
- if (dir === 'after') {
889
- if (orEqual ? tv < tb : tv <= tb)
890
- return new Date(tb + 1000).toISOString();
891
- }
892
- else if (orEqual ? tv > tb : tv >= tb) {
893
- return new Date(tb - 1000).toISOString();
894
- }
895
- return v;
896
- }
897
- if (/^\d{4}-\d{2}-\d{2}$/.test(v) && /^\d{4}-\d{2}-\d{2}$/.test(b)) {
898
- const dv = Date.parse(`${v}T00:00:00Z`);
899
- const db = Date.parse(`${b}T00:00:00Z`);
900
- if (dir === 'after') {
901
- if (orEqual ? dv < db : dv <= db)
902
- return new Date(db + 86400_000).toISOString().slice(0, 10);
903
- }
904
- else if (orEqual ? dv > db : dv >= db) {
905
- return new Date(db - 86400_000).toISOString().slice(0, 10);
906
- }
907
- return v;
908
- }
909
- }
910
- if (typeof v === 'number' && typeof b === 'number') {
911
- if (dir === 'after')
912
- return orEqual ? Math.max(v, b) : Math.max(v, b + 1);
913
- return orEqual ? Math.min(v, b) : Math.min(v, b - 1);
914
- }
915
- return v;
916
- }
917
- /** Apply per-row ordering CHECKs (e.g. ExpiresAt > CreatedAt) in place. */
918
- function applyOrdering(fields, ordering) {
919
- if (!ordering.size)
920
- return;
921
- const byCol = new Map(fields.map((f) => [f.column, f]));
922
- for (const f of fields) {
923
- const rel = ordering.get(f.column);
924
- if (!rel)
925
- continue;
926
- if (rel.after && f.value != null) {
927
- const other = byCol.get(rel.after.column);
928
- if (other && other.value != null)
929
- f.value = orderedValue(f.value, other.value, 'after', rel.after.orEqual);
930
- }
931
- if (rel.before && f.value != null) {
932
- const other = byCol.get(rel.before.column);
933
- if (other && other.value != null)
934
- f.value = orderedValue(f.value, other.value, 'before', rel.before.orEqual);
935
- }
936
- }
937
- }
938
- async function generateRows(db, info, configs, count, plan = {}) {
939
- const fkByColumn = new Map();
940
- for (const fk of info.foreignKeys)
941
- fkByColumn.set(fk.from, fk);
942
- // Columns with a single-column UNIQUE index must get distinct values.
943
- const uniqueCols = new Set();
944
- for (const cfg of configs)
945
- if (cfg.unique)
946
- uniqueCols.add(cfg.name);
947
- // CHECK constraints (JSON whitelists + column ordering).
948
- const checkInfo = parseChecks(info.sql ?? null);
949
- const warnings = [];
950
- const prepared = [];
951
- const fkPools = new Map();
952
- // A NOT NULL FK whose referenced table is empty cannot be satisfied (SQLite
953
- // enforces FKs) — the column is omitted from generated rows with a clear
954
- // warning so the user knows to seed the referenced table first.
955
- const fkOmit = new Set();
956
- for (const col of info.columns) {
957
- const cfg = configs.find((c) => c.name === col.name);
958
- const raw = plan[col.name];
959
- const p = raw && raw.strategy ? raw : (cfg?.defaultPlan ?? { strategy: 'skip' });
960
- if (!cfg || !cfg.strategies.some((s) => s.id === p.strategy)) {
961
- throw new Error(`Unsupported generator strategy "${p.strategy}" for column "${col.name}".`);
962
- }
963
- if (p.strategy === 'list' && !(p.values ?? []).some((v) => String(v).trim() !== '')) {
964
- warnings.push(`Column "${col.name}": the list is empty, so it will be skipped (DB default applies).`);
965
- }
966
- if ((p.strategy === 'skip' || p.strategy === 'null') && !col.pk && col.notnull && col.dflt_value == null) {
967
- warnings.push(`Column "${col.name}" is NOT NULL without a default — "${p.strategy}" rows may fail to insert.`);
968
- }
969
- if (p.strategy === 'fk') {
970
- const pool = await sampleFkValues(db, fkByColumn.get(col.name) ?? null);
971
- fkPools.set(col.name, pool);
972
- const refTable = fkByColumn.get(col.name)?.table ?? '?';
973
- if (!pool.length) {
974
- if (col.notnull && !col.pk) {
975
- fkOmit.add(col.name);
976
- warnings.push(`Column "${col.name}" is a NOT NULL foreign key to "${refTable}", which has no rows. It is omitted from generated rows — seed "${refTable}" first so the insert can reference real values.`);
977
- }
978
- else {
979
- warnings.push(`Column "${col.name}": "${refTable}" is empty, so NULL will be inserted.`);
980
- }
981
- }
982
- }
983
- prepared.push({ col, plan: p });
984
- }
985
- const rows = [];
986
- const uniqueSeen = new Map();
987
- for (let i = 0; i < count; i++) {
988
- const fields = [];
989
- for (const { col, plan: p } of prepared) {
990
- if (p.strategy === 'fk' && fkOmit.has(col.name))
991
- continue;
992
- let v = generateOne(col, p, fkPools.get(col.name));
993
- if (v === SKIP)
994
- continue;
995
- if (uniqueCols.has(col.name)) {
996
- const seen = uniqueSeen.get(col.name) ?? new Set();
997
- let attempts = 0;
998
- while (seen.has(stringifyKey(v)) && attempts < 20) {
999
- v = generateOne(col, p, fkPools.get(col.name));
1000
- attempts += 1;
1001
- }
1002
- if (seen.has(stringifyKey(v))) {
1003
- warnings.push(`Column "${col.name}" is UNIQUE but the generator kept colliding — appended a suffix to keep rows insertable.`);
1004
- v = uniqueSuffix(v, seen);
1005
- }
1006
- seen.add(stringifyKey(v));
1007
- uniqueSeen.set(col.name, seen);
1008
- }
1009
- fields.push({ column: col.name, value: v });
1010
- }
1011
- // Honor CHECK ordering constraints (e.g. ExpiresAt > CreatedAt) so rows pass.
1012
- applyOrdering(fields, checkInfo.ordering);
1013
- rows.push(fields);
1014
- }
1015
- return { rows, warnings };
1016
- }
1017
- /**
1018
- * Render generated rows as a sequence of INSERT statements for preview
1019
- * ("generate but never execute"). Each row is its own statement so per-row
1020
- * column lists (with skips) stay valid.
1021
- */
1022
- function buildSeedInsertSql(table, rows) {
1023
- return rows
1024
- .map((fields) => {
1025
- const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column));
1026
- const vals = fields.map((f) => (0, generator_1.sqlValue)(f.value));
1027
- return `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${vals.join(', ')});`;
1028
- })
1029
- .join('\n');
1030
- }
1031
- //# sourceMappingURL=generator.js.map