sqlite-hub 1.4.0 → 2.0.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 (46) hide show
  1. package/README.md +14 -1
  2. package/bin/sqlite-hub-mcp.js +8 -0
  3. package/bin/sqlite-hub.js +555 -122
  4. package/docs/API.md +30 -0
  5. package/docs/CLI.md +22 -0
  6. package/docs/CLI_API_PARITY.md +61 -0
  7. package/docs/MCP.md +85 -0
  8. package/docs/changelog.md +13 -1
  9. package/docs/guidelines/AGENTS.md +32 -0
  10. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  11. package/docs/todo.md +1 -14
  12. package/frontend/js/api.js +49 -0
  13. package/frontend/js/app.js +202 -2
  14. package/frontend/js/components/connectionCard.js +3 -1
  15. package/frontend/js/components/modal.js +356 -15
  16. package/frontend/js/components/sidebar.js +41 -7
  17. package/frontend/js/components/topNav.js +2 -14
  18. package/frontend/js/router.js +10 -0
  19. package/frontend/js/store.js +483 -7
  20. package/frontend/js/utils/inputClear.js +36 -0
  21. package/frontend/js/utils/syntheticData.js +240 -0
  22. package/frontend/js/views/backups.js +52 -0
  23. package/frontend/js/views/data.js +16 -0
  24. package/frontend/js/views/logs.js +339 -0
  25. package/frontend/js/views/settings.js +266 -30
  26. package/frontend/js/views/structure.js +6 -0
  27. package/frontend/js/views/tableAdvisor.js +385 -0
  28. package/frontend/js/views/tableDesigner.js +6 -0
  29. package/frontend/styles/components.css +149 -0
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/views.css +31 -0
  32. package/package.json +4 -2
  33. package/server/mcp/stdioServer.js +272 -0
  34. package/server/routes/data.js +45 -0
  35. package/server/routes/externalApi.js +285 -2
  36. package/server/routes/logs.js +127 -0
  37. package/server/routes/settings.js +47 -0
  38. package/server/server.js +3 -0
  39. package/server/services/databaseCommandService.js +284 -2
  40. package/server/services/mcpStatusService.js +140 -0
  41. package/server/services/mcpToolService.js +274 -0
  42. package/server/services/sqlite/dataBrowserService.js +36 -0
  43. package/server/services/sqlite/introspection.js +226 -22
  44. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  45. package/server/services/sqlite/tableAdvisor.js +790 -0
  46. package/server/services/storage/appStateStore.js +821 -169
@@ -0,0 +1,728 @@
1
+ const { randomUUID } = require("node:crypto");
2
+ const { ValidationError } = require("../../utils/errors");
3
+ const { quoteIdentifier } = require("../../utils/identifier");
4
+
5
+ const DEFAULT_ROW_COUNT = 100;
6
+ const MAX_ROW_COUNT = 10000;
7
+ const PREVIEW_ROW_COUNT = 3;
8
+ const GENERATOR_TYPES = new Set([
9
+ "skip",
10
+ "static",
11
+ "randomText",
12
+ "name",
13
+ "firstName",
14
+ "lastName",
15
+ "email",
16
+ "username",
17
+ "title",
18
+ "slug",
19
+ "url",
20
+ "randomInteger",
21
+ "randomDecimal",
22
+ "boolean",
23
+ "timestamp",
24
+ "uuid",
25
+ "oneOf",
26
+ "existingForeignKey",
27
+ ]);
28
+
29
+ const FIRST_NAMES = [
30
+ "Ada",
31
+ "Grace",
32
+ "Linus",
33
+ "Margaret",
34
+ "Donald",
35
+ "Barbara",
36
+ "Ken",
37
+ "Edsger",
38
+ "Radia",
39
+ "Tim",
40
+ ];
41
+ const LAST_NAMES = [
42
+ "Lovelace",
43
+ "Hopper",
44
+ "Torvalds",
45
+ "Hamilton",
46
+ "Knuth",
47
+ "Liskov",
48
+ "Thompson",
49
+ "Dijkstra",
50
+ "Perlman",
51
+ "Berners-Lee",
52
+ ];
53
+ const WORDS = [
54
+ "alpha",
55
+ "vector",
56
+ "signal",
57
+ "orbit",
58
+ "matrix",
59
+ "delta",
60
+ "pixel",
61
+ "ledger",
62
+ "kernel",
63
+ "index",
64
+ "stream",
65
+ "module",
66
+ ];
67
+
68
+ function randomItem(values) {
69
+ return values[Math.floor(Math.random() * values.length)];
70
+ }
71
+
72
+ function randomInteger(min, max) {
73
+ return Math.floor(Math.random() * (max - min + 1)) + min;
74
+ }
75
+
76
+ function slugify(value) {
77
+ return String(value ?? "")
78
+ .toLowerCase()
79
+ .replace(/[^a-z0-9]+/g, "-")
80
+ .replace(/^-+|-+$/g, "");
81
+ }
82
+
83
+ function normalizeRowCount(value) {
84
+ const rowCount = Number(value ?? DEFAULT_ROW_COUNT);
85
+
86
+ if (!Number.isInteger(rowCount) || rowCount < 1 || rowCount > MAX_ROW_COUNT) {
87
+ throw new ValidationError(`rowCount must be an integer between 1 and ${MAX_ROW_COUNT}.`);
88
+ }
89
+
90
+ return rowCount;
91
+ }
92
+
93
+ function isIntegerPrimaryKeyColumn(column) {
94
+ return (
95
+ Number(column.primaryKeyPosition ?? 0) > 0 &&
96
+ (column.affinity === "INTEGER" || /\bINT\b/i.test(column.declaredType ?? ""))
97
+ );
98
+ }
99
+
100
+ function hasDefaultValue(column) {
101
+ return column.defaultValue !== null && column.defaultValue !== undefined;
102
+ }
103
+
104
+ function canSkipColumn(column) {
105
+ if (!column.visible || column.generated) {
106
+ return true;
107
+ }
108
+
109
+ if (isIntegerPrimaryKeyColumn(column)) {
110
+ return true;
111
+ }
112
+
113
+ if (hasDefaultValue(column)) {
114
+ return true;
115
+ }
116
+
117
+ return !column.notNull && Number(column.primaryKeyPosition ?? 0) === 0;
118
+ }
119
+
120
+ function normalizeName(value) {
121
+ return String(value ?? "")
122
+ .trim()
123
+ .toLowerCase();
124
+ }
125
+
126
+ function hasBooleanAllowedValues(column) {
127
+ const values = (column.allowedValues ?? []).map((value) => String(value));
128
+ const uniqueValues = new Set(values);
129
+
130
+ return uniqueValues.size === 2 && uniqueValues.has("0") && uniqueValues.has("1");
131
+ }
132
+
133
+ function hasBooleanIntegerRange(column) {
134
+ return Number(column.integerRange?.min) === 0 && Number(column.integerRange?.max) === 1;
135
+ }
136
+
137
+ function isBooleanLikeColumn(column, normalizedName) {
138
+ return (
139
+ /(^is_|^has_|enabled$|active$|archived$|published$|deleted$|visible$|boolean|bool)/.test(
140
+ normalizedName
141
+ ) ||
142
+ hasBooleanAllowedValues(column) ||
143
+ hasBooleanIntegerRange(column)
144
+ );
145
+ }
146
+
147
+ function getForeignKeyInfo(tableDetail, columnName) {
148
+ const foreignKeys = tableDetail?.foreignKeys ?? [];
149
+ const singleColumnForeignKey = foreignKeys.find(
150
+ (foreignKey) =>
151
+ (foreignKey.mappings?.length ?? 0) === 1 && foreignKey.mappings?.[0]?.from === columnName
152
+ );
153
+
154
+ if (singleColumnForeignKey) {
155
+ return {
156
+ kind: "single",
157
+ foreignKey: singleColumnForeignKey,
158
+ mapping: singleColumnForeignKey.mappings[0],
159
+ };
160
+ }
161
+
162
+ const compositeForeignKey = foreignKeys.find((foreignKey) =>
163
+ (foreignKey.mappings ?? []).some((mapping) => mapping.from === columnName)
164
+ );
165
+
166
+ if (compositeForeignKey) {
167
+ return {
168
+ kind: "composite",
169
+ foreignKey: compositeForeignKey,
170
+ mapping: compositeForeignKey.mappings.find((mapping) => mapping.from === columnName) ?? null,
171
+ };
172
+ }
173
+
174
+ return null;
175
+ }
176
+
177
+ function suggestGeneratorForColumn(column, tableDetail = null) {
178
+ const name = normalizeName(column.name);
179
+ const declaredType = String(column.declaredType ?? "").toUpperCase();
180
+ const affinity = String(column.affinity ?? "").toUpperCase();
181
+
182
+ if (!column.visible || column.generated || affinity === "BLOB") {
183
+ return "skip";
184
+ }
185
+
186
+ if (isIntegerPrimaryKeyColumn(column)) {
187
+ return "skip";
188
+ }
189
+
190
+ const foreignKeyInfo = getForeignKeyInfo(tableDetail, column.name);
191
+
192
+ if (foreignKeyInfo?.kind === "single") {
193
+ return "existingForeignKey";
194
+ }
195
+
196
+ if (foreignKeyInfo?.kind === "composite") {
197
+ return "skip";
198
+ }
199
+
200
+ if (/(^|_)email$/.test(name) || name.includes("email_address")) {
201
+ return "email";
202
+ }
203
+
204
+ if (/^(first_name|firstname|given_name)$/.test(name)) {
205
+ return "firstName";
206
+ }
207
+
208
+ if (/^(last_name|lastname|family_name|surname)$/.test(name)) {
209
+ return "lastName";
210
+ }
211
+
212
+ if (/^(name|full_name|display_name|contact_name)$/.test(name)) {
213
+ return "name";
214
+ }
215
+
216
+ if (/^(username|user_name|login)$/.test(name)) {
217
+ return "username";
218
+ }
219
+
220
+ if (/(^|_)(title|headline|subject)$/.test(name)) {
221
+ return "title";
222
+ }
223
+
224
+ if (/(^|_)slug$/.test(name)) {
225
+ return "slug";
226
+ }
227
+
228
+ if (/(^|_)(url|website|site|homepage)$/.test(name)) {
229
+ return "url";
230
+ }
231
+
232
+ if (/(^|_)(uuid|guid)$/.test(name)) {
233
+ return "uuid";
234
+ }
235
+
236
+ if (isBooleanLikeColumn(column, name)) {
237
+ return "boolean";
238
+ }
239
+
240
+ if ((column.allowedValues ?? []).length) {
241
+ return "oneOf";
242
+ }
243
+
244
+ if (/(date|time|created_at|updated_at|timestamp)/.test(name) || /DATE|TIME/.test(declaredType)) {
245
+ return "timestamp";
246
+ }
247
+
248
+ if (affinity === "INTEGER") {
249
+ return "randomInteger";
250
+ }
251
+
252
+ if (affinity === "REAL" || affinity === "NUMERIC" || /DECIMAL|NUMERIC/.test(declaredType)) {
253
+ return "randomDecimal";
254
+ }
255
+
256
+ if (affinity === "TEXT") {
257
+ return "randomText";
258
+ }
259
+
260
+ return "skip";
261
+ }
262
+
263
+ function toFiniteNumber(value, fallback, label) {
264
+ const number = value === "" || value === null || value === undefined ? fallback : Number(value);
265
+
266
+ if (!Number.isFinite(number)) {
267
+ throw new ValidationError(`${label} must be a number.`);
268
+ }
269
+
270
+ return number;
271
+ }
272
+
273
+ function toInteger(value, fallback, label) {
274
+ const number = toFiniteNumber(value, fallback, label);
275
+
276
+ if (!Number.isInteger(number)) {
277
+ throw new ValidationError(`${label} must be an integer.`);
278
+ }
279
+
280
+ return number;
281
+ }
282
+
283
+ function normalizeIntegerRange(column = {}) {
284
+ const min = Number(column.integerRange?.min);
285
+ const max = Number(column.integerRange?.max);
286
+
287
+ return {
288
+ min: Number.isSafeInteger(min) ? min : null,
289
+ max: Number.isSafeInteger(max) ? max : null,
290
+ };
291
+ }
292
+
293
+ function getDefaultIntegerOptions(column = {}) {
294
+ const range = normalizeIntegerRange(column);
295
+ const min =
296
+ range.min ??
297
+ (range.max !== null && range.max < 1
298
+ ? range.max - 999
299
+ : 1);
300
+ const max =
301
+ range.max ??
302
+ (range.min !== null && range.min > 1000
303
+ ? range.min + 999
304
+ : 1000);
305
+
306
+ return {
307
+ min,
308
+ max,
309
+ rangeMin: range.min,
310
+ rangeMax: range.max,
311
+ };
312
+ }
313
+
314
+ function normalizeCommaValues(value, fallback = []) {
315
+ const source = String(value ?? "").trim();
316
+ const values = source
317
+ ? source
318
+ .split(",")
319
+ .map((entry) => entry.trim())
320
+ .filter(Boolean)
321
+ : fallback.map((entry) => String(entry));
322
+
323
+ if (!values.length) {
324
+ throw new ValidationError("One Of needs at least one value.");
325
+ }
326
+
327
+ return values;
328
+ }
329
+
330
+ function normalizeTimestampOptions(options = {}) {
331
+ const range = ["last30", "last365", "custom"].includes(options.range)
332
+ ? options.range
333
+ : "last30";
334
+
335
+ if (range !== "custom") {
336
+ return { range };
337
+ }
338
+
339
+ const from = options.from ? new Date(options.from) : null;
340
+ const to = options.to ? new Date(options.to) : null;
341
+
342
+ if ((from && Number.isNaN(from.getTime())) || (to && Number.isNaN(to.getTime()))) {
343
+ throw new ValidationError("Custom timestamp range must use valid date values.");
344
+ }
345
+
346
+ if (from && to && from.getTime() > to.getTime()) {
347
+ throw new ValidationError("Custom timestamp range start must be before end.");
348
+ }
349
+
350
+ return {
351
+ range,
352
+ from: from ? from.toISOString() : null,
353
+ to: to ? to.toISOString() : null,
354
+ };
355
+ }
356
+
357
+ function normalizeExistingForeignKeyOptions(tableDetail, column) {
358
+ const foreignKeyInfo = getForeignKeyInfo(tableDetail, column.name);
359
+
360
+ if (foreignKeyInfo?.kind === "composite") {
361
+ throw new ValidationError(
362
+ `${column.name} is part of a composite foreign key. Synthetic FK generation supports single-column foreign keys only.`
363
+ );
364
+ }
365
+
366
+ if (foreignKeyInfo?.kind !== "single") {
367
+ throw new ValidationError(`${column.name} does not have a single-column foreign key.`);
368
+ }
369
+
370
+ return {
371
+ referencedTable: foreignKeyInfo.foreignKey.referencedTable,
372
+ referencedColumn: foreignKeyInfo.mapping.to,
373
+ };
374
+ }
375
+
376
+ function normalizeGeneratorOptions(generator, column, options = {}, tableDetail = null) {
377
+ switch (generator) {
378
+ case "static":
379
+ return { value: options.value ?? "" };
380
+ case "randomInteger": {
381
+ const defaults = getDefaultIntegerOptions(column);
382
+ const min = toInteger(options.min, defaults.min, "Integer min");
383
+ const max = toInteger(options.max, defaults.max, "Integer max");
384
+
385
+ if (min > max) {
386
+ throw new ValidationError("Integer min must be less than or equal to max.");
387
+ }
388
+
389
+ if (defaults.rangeMin !== null && min < defaults.rangeMin) {
390
+ throw new ValidationError(`Integer min must be greater than or equal to ${defaults.rangeMin}.`);
391
+ }
392
+
393
+ if (defaults.rangeMax !== null && max > defaults.rangeMax) {
394
+ throw new ValidationError(`Integer max must be less than or equal to ${defaults.rangeMax}.`);
395
+ }
396
+
397
+ return { min, max };
398
+ }
399
+ case "randomDecimal": {
400
+ const min = toFiniteNumber(options.min, 0, "Decimal min");
401
+ const max = toFiniteNumber(options.max, 1000, "Decimal max");
402
+ const decimals = toInteger(options.decimals, 2, "Decimal places");
403
+
404
+ if (min > max) {
405
+ throw new ValidationError("Decimal min must be less than or equal to max.");
406
+ }
407
+
408
+ if (decimals < 0 || decimals > 8) {
409
+ throw new ValidationError("Decimal places must be between 0 and 8.");
410
+ }
411
+
412
+ return { min, max, decimals };
413
+ }
414
+ case "boolean": {
415
+ const trueProbability = toFiniteNumber(options.trueProbability, 50, "True probability");
416
+
417
+ if (trueProbability < 0 || trueProbability > 100) {
418
+ throw new ValidationError("True probability must be between 0 and 100.");
419
+ }
420
+
421
+ return { trueProbability };
422
+ }
423
+ case "timestamp":
424
+ return normalizeTimestampOptions(options);
425
+ case "oneOf":
426
+ return { values: normalizeCommaValues(options.values, column.allowedValues ?? []) };
427
+ case "existingForeignKey":
428
+ return normalizeExistingForeignKeyOptions(tableDetail, column);
429
+ default:
430
+ return {};
431
+ }
432
+ }
433
+
434
+ function normalizeMappings(tableDetail, mappings = []) {
435
+ const candidateColumns = (tableDetail.columns ?? []).filter(
436
+ (column) => column.visible && !column.generated
437
+ );
438
+ const columnsByName = new Map(candidateColumns.map((column) => [column.name, column]));
439
+ const providedMappings = new Map();
440
+
441
+ (Array.isArray(mappings) ? mappings : []).forEach((mapping) => {
442
+ const columnName = String(mapping?.columnName ?? "");
443
+
444
+ if (!columnsByName.has(columnName)) {
445
+ throw new ValidationError(`Unknown generated data column: ${columnName || "(empty)"}.`);
446
+ }
447
+
448
+ providedMappings.set(columnName, mapping);
449
+ });
450
+
451
+ return candidateColumns.map((column) => {
452
+ const provided = providedMappings.get(column.name) ?? {};
453
+ const requestedGenerator = provided.generator ?? suggestGeneratorForColumn(column, tableDetail);
454
+ const generator = GENERATOR_TYPES.has(requestedGenerator) ? requestedGenerator : null;
455
+
456
+ if (!generator) {
457
+ throw new ValidationError(`Unsupported generator for ${column.name}: ${requestedGenerator}.`);
458
+ }
459
+
460
+ if (generator === "skip" && !canSkipColumn(column)) {
461
+ if (getForeignKeyInfo(tableDetail, column.name)?.kind === "composite") {
462
+ throw new ValidationError(
463
+ `${column.name} is part of a composite foreign key. Synthetic FK generation supports single-column foreign keys only.`
464
+ );
465
+ }
466
+
467
+ throw new ValidationError(
468
+ `${column.name} is required and cannot be skipped without a default value.`
469
+ );
470
+ }
471
+
472
+ return {
473
+ column,
474
+ columnName: column.name,
475
+ generator,
476
+ options: normalizeGeneratorOptions(generator, column, provided.options ?? {}, tableDetail),
477
+ };
478
+ });
479
+ }
480
+
481
+ function buildForeignKeyPoolKey(options) {
482
+ return `${options.referencedTable}\u0000${options.referencedColumn}`;
483
+ }
484
+
485
+ function resolveForeignKeyPools(db, mappings) {
486
+ const pools = new Map();
487
+ const foreignKeyMappings = mappings.filter((mapping) => mapping.generator === "existingForeignKey");
488
+
489
+ foreignKeyMappings.forEach((mapping) => {
490
+ const poolKey = buildForeignKeyPoolKey(mapping.options);
491
+
492
+ mapping.options.poolKey = poolKey;
493
+
494
+ if (pools.has(poolKey)) {
495
+ return;
496
+ }
497
+
498
+ const rows = db
499
+ .prepare(
500
+ [
501
+ "SELECT DISTINCT",
502
+ `${quoteIdentifier(mapping.options.referencedColumn)} AS value`,
503
+ "FROM",
504
+ quoteIdentifier(mapping.options.referencedTable),
505
+ "WHERE",
506
+ `${quoteIdentifier(mapping.options.referencedColumn)} IS NOT NULL`,
507
+ "LIMIT 1000",
508
+ ].join(" ")
509
+ )
510
+ .all();
511
+
512
+ pools.set(poolKey, {
513
+ values: rows.map((row) => row.value),
514
+ });
515
+ });
516
+
517
+ foreignKeyMappings.forEach((mapping) => {
518
+ const pool = pools.get(mapping.options.poolKey);
519
+
520
+ if (!pool?.values?.length && mapping.column.notNull && !hasDefaultValue(mapping.column)) {
521
+ throw new ValidationError(
522
+ `${mapping.columnName} references ${mapping.options.referencedTable}.${mapping.options.referencedColumn}, but no parent values exist.`
523
+ );
524
+ }
525
+ });
526
+
527
+ return pools;
528
+ }
529
+
530
+ function buildPerson(index) {
531
+ const firstName = FIRST_NAMES[index % FIRST_NAMES.length];
532
+ const lastName = LAST_NAMES[(index * 3) % LAST_NAMES.length];
533
+
534
+ return {
535
+ firstName,
536
+ lastName,
537
+ fullName: `${firstName} ${lastName}`,
538
+ };
539
+ }
540
+
541
+ function buildTitle() {
542
+ const words = [randomItem(WORDS), randomItem(WORDS), randomItem(WORDS)];
543
+
544
+ return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
545
+ }
546
+
547
+ function buildTimestamp(options) {
548
+ const now = Date.now();
549
+ let from = now - 30 * 24 * 60 * 60 * 1000;
550
+ let to = now;
551
+
552
+ if (options.range === "last365") {
553
+ from = now - 365 * 24 * 60 * 60 * 1000;
554
+ } else if (options.range === "custom") {
555
+ from = options.from ? new Date(options.from).getTime() : from;
556
+ to = options.to ? new Date(options.to).getTime() : to;
557
+ }
558
+
559
+ const value = new Date(randomInteger(Math.floor(from), Math.floor(to)));
560
+
561
+ return value.toISOString().slice(0, 19).replace("T", " ");
562
+ }
563
+
564
+ function generateValue(mapping, index, context = {}) {
565
+ const person = buildPerson(index);
566
+ const serial = index + 1;
567
+
568
+ switch (mapping.generator) {
569
+ case "static":
570
+ return mapping.options.value;
571
+ case "randomText":
572
+ return `${buildTitle()} ${randomItem(WORDS)} ${randomItem(WORDS)}.`;
573
+ case "name":
574
+ return person.fullName;
575
+ case "firstName":
576
+ return person.firstName;
577
+ case "lastName":
578
+ return person.lastName;
579
+ case "email":
580
+ return `${slugify(`${person.firstName}.${person.lastName}.${serial}`)}@example.test`;
581
+ case "username":
582
+ return slugify(`${person.firstName}${person.lastName}${serial}`).replace(/-/g, "");
583
+ case "title":
584
+ return buildTitle();
585
+ case "slug":
586
+ return `${slugify(buildTitle())}-${serial}`;
587
+ case "url":
588
+ return `https://example.com/${slugify(buildTitle())}-${serial}`;
589
+ case "randomInteger":
590
+ return randomInteger(mapping.options.min, mapping.options.max);
591
+ case "randomDecimal": {
592
+ const value = mapping.options.min + Math.random() * (mapping.options.max - mapping.options.min);
593
+
594
+ return Number(value.toFixed(mapping.options.decimals));
595
+ }
596
+ case "boolean":
597
+ return Math.random() * 100 < mapping.options.trueProbability ? 1 : 0;
598
+ case "timestamp":
599
+ return buildTimestamp(mapping.options);
600
+ case "uuid":
601
+ return randomUUID();
602
+ case "oneOf":
603
+ return randomItem(mapping.options.values);
604
+ case "existingForeignKey": {
605
+ const pool = context.foreignKeyPools?.get(mapping.options.poolKey);
606
+
607
+ if (pool?.values?.length) {
608
+ return randomItem(pool.values);
609
+ }
610
+
611
+ return null;
612
+ }
613
+ case "skip":
614
+ default:
615
+ return null;
616
+ }
617
+ }
618
+
619
+ function serializeMappingOptions(options = {}) {
620
+ const { poolKey, ...publicOptions } = options;
621
+
622
+ return publicOptions;
623
+ }
624
+
625
+ function shouldInsertMapping(mapping, context = {}) {
626
+ if (mapping.generator === "skip") {
627
+ return false;
628
+ }
629
+
630
+ if (mapping.generator !== "existingForeignKey") {
631
+ return true;
632
+ }
633
+
634
+ const pool = context.foreignKeyPools?.get(mapping.options.poolKey);
635
+
636
+ return Boolean(pool?.values?.length) || !hasDefaultValue(mapping.column);
637
+ }
638
+
639
+ function buildSyntheticRows(db, tableDetail, payload = {}, options = {}) {
640
+ const rowCount = normalizeRowCount(payload.rowCount);
641
+ const limit = Number.isInteger(options.limit) ? Math.min(options.limit, rowCount) : rowCount;
642
+ const mappings = normalizeMappings(tableDetail, payload.mappings);
643
+ const foreignKeyPools = resolveForeignKeyPools(db, mappings);
644
+ const context = { foreignKeyPools };
645
+ const columns = mappings.map((mapping) => mapping.columnName);
646
+ const rows = Array.from({ length: limit }, (_value, index) =>
647
+ Object.fromEntries(
648
+ mappings.map((mapping) => [
649
+ mapping.columnName,
650
+ shouldInsertMapping(mapping, context) ? generateValue(mapping, index, context) : null,
651
+ ])
652
+ )
653
+ );
654
+
655
+ return {
656
+ rowCount,
657
+ previewRowCount: limit,
658
+ columns,
659
+ rows,
660
+ mappings: mappings.map((mapping) => ({
661
+ columnName: mapping.columnName,
662
+ generator: mapping.generator,
663
+ options: serializeMappingOptions(mapping.options),
664
+ })),
665
+ };
666
+ }
667
+
668
+ function insertSyntheticRows(db, tableDetail, payload = {}) {
669
+ const rowCount = normalizeRowCount(payload.rowCount);
670
+ const mappings = normalizeMappings(tableDetail, payload.mappings);
671
+ const foreignKeyPools = resolveForeignKeyPools(db, mappings);
672
+ const context = { foreignKeyPools };
673
+ const insertMappings = mappings.filter((mapping) => shouldInsertMapping(mapping, context));
674
+
675
+ if (!insertMappings.length) {
676
+ const insertDefault = db.prepare(
677
+ ["INSERT INTO", quoteIdentifier(tableDetail.name), "DEFAULT VALUES"].join(" ")
678
+ );
679
+ const insertManyDefaults = db.transaction(() => {
680
+ for (let index = 0; index < rowCount; index += 1) {
681
+ insertDefault.run();
682
+ }
683
+ });
684
+
685
+ insertManyDefaults();
686
+
687
+ return {
688
+ tableName: tableDetail.name,
689
+ insertedRowCount: rowCount,
690
+ columns: [],
691
+ };
692
+ }
693
+
694
+ const columnNames = insertMappings.map((mapping) => mapping.columnName);
695
+ const placeholders = columnNames.map(() => "?").join(", ");
696
+ const statement = db.prepare(
697
+ [
698
+ "INSERT INTO",
699
+ quoteIdentifier(tableDetail.name),
700
+ `(${columnNames.map((columnName) => quoteIdentifier(columnName)).join(", ")})`,
701
+ "VALUES",
702
+ `(${placeholders})`,
703
+ ].join(" ")
704
+ );
705
+ const insertMany = db.transaction(() => {
706
+ for (let index = 0; index < rowCount; index += 1) {
707
+ statement.run(insertMappings.map((mapping) => generateValue(mapping, index, context)));
708
+ }
709
+ });
710
+
711
+ insertMany();
712
+
713
+ return {
714
+ tableName: tableDetail.name,
715
+ insertedRowCount: rowCount,
716
+ columns: columnNames,
717
+ };
718
+ }
719
+
720
+ module.exports = {
721
+ DEFAULT_ROW_COUNT,
722
+ MAX_ROW_COUNT,
723
+ PREVIEW_ROW_COUNT,
724
+ buildSyntheticRows,
725
+ insertSyntheticRows,
726
+ isIntegerPrimaryKeyColumn,
727
+ suggestGeneratorForColumn,
728
+ };