sqlite-hub 0.9.3 → 0.9.4
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.
- package/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +2 -158
- package/frontend/js/app.js +5 -1
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +145 -195
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +81 -99
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +28 -31
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/store.js +39 -3
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +23 -43
- package/frontend/js/views/data.js +116 -132
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/tailwind.css +81 -0
- package/frontend/styles/tailwind.generated.css +1 -0
- package/frontend/styles/tokens.css +3 -3
- package/package.json +16 -3
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -84
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- package/shortkeys.md +0 -5
|
@@ -3,6 +3,7 @@ const path = require("node:path");
|
|
|
3
3
|
const { fileURLToPath } = require("node:url");
|
|
4
4
|
const { detectQueryType } = require("../storage/queryHistoryUtils");
|
|
5
5
|
const { ConflictError, NotFoundError, ValidationError, mapSqliteError } = require("../../utils/errors");
|
|
6
|
+
const { resolvePathInsideDirectory } = require("../../utils/fileValidation");
|
|
6
7
|
const { quoteIdentifier } = require("../../utils/identifier");
|
|
7
8
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
8
9
|
const { serializeRow } = require("../../utils/sqliteTypes");
|
|
@@ -314,16 +315,18 @@ function buildDefaultMediaQueries(tableDetail, config = {}) {
|
|
|
314
315
|
|
|
315
316
|
const selectionPrefix =
|
|
316
317
|
tableDetail.identityStrategy?.type === "rowid"
|
|
317
|
-
?
|
|
318
|
+
? ["SELECT rowid AS", quoteIdentifier(MEDIA_ROWID_ALIAS), ", *"].join(" ")
|
|
318
319
|
: "SELECT *";
|
|
319
|
-
const tableSql =
|
|
320
|
+
const tableSql = [selectionPrefix, "FROM", quoteIdentifier(tableDetail.name)].join(" ");
|
|
320
321
|
const taggedColumnSql = quoteIdentifier(config.taggedColumn);
|
|
321
322
|
const orderByClause = buildOrderByClause(tableDetail, MEDIA_ROWID_ALIAS);
|
|
322
|
-
const orderSegment = orderByClause ?
|
|
323
|
+
const orderSegment = orderByClause ? "\n" + orderByClause : "";
|
|
323
324
|
|
|
324
325
|
return {
|
|
325
|
-
untaggedQuery:
|
|
326
|
-
|
|
326
|
+
untaggedQuery:
|
|
327
|
+
tableSql + "\nWHERE COALESCE(" + taggedColumnSql + ", 0) = 0" + orderSegment,
|
|
328
|
+
taggedQuery:
|
|
329
|
+
tableSql + "\nWHERE COALESCE(" + taggedColumnSql + ", 0) = 1" + orderSegment,
|
|
327
330
|
};
|
|
328
331
|
}
|
|
329
332
|
|
|
@@ -341,7 +344,11 @@ function buildWrappedQuery(query) {
|
|
|
341
344
|
|
|
342
345
|
function getQueryOutputColumns(db, query) {
|
|
343
346
|
const statement = db.prepare(
|
|
344
|
-
|
|
347
|
+
[
|
|
348
|
+
"SELECT * FROM (",
|
|
349
|
+
buildWrappedQuery(query),
|
|
350
|
+
") AS media_tagging_source LIMIT 0",
|
|
351
|
+
].join("")
|
|
345
352
|
);
|
|
346
353
|
|
|
347
354
|
return statement.columns().map((column) => column.name);
|
|
@@ -349,7 +356,13 @@ function getQueryOutputColumns(db, query) {
|
|
|
349
356
|
|
|
350
357
|
function countQueryRows(db, query) {
|
|
351
358
|
const row = db
|
|
352
|
-
.prepare(
|
|
359
|
+
.prepare(
|
|
360
|
+
[
|
|
361
|
+
"SELECT COUNT(*) AS count FROM (",
|
|
362
|
+
buildWrappedQuery(query),
|
|
363
|
+
") AS media_tagging_source",
|
|
364
|
+
].join("")
|
|
365
|
+
)
|
|
353
366
|
.get();
|
|
354
367
|
|
|
355
368
|
return Number(row?.count ?? 0);
|
|
@@ -962,16 +975,21 @@ class MediaTaggingService {
|
|
|
962
975
|
const labelColumns = getTagLabelColumns(tagTableDetail);
|
|
963
976
|
const selectionPrefix =
|
|
964
977
|
tagTableDetail.identityStrategy?.type === "rowid"
|
|
965
|
-
?
|
|
978
|
+
? ["SELECT rowid AS", quoteIdentifier(TAG_ROWID_ALIAS), ", *"].join(" ")
|
|
966
979
|
: "SELECT *";
|
|
967
980
|
const orderByColumns = labelColumns.length ? labelColumns : keyColumns;
|
|
968
981
|
const parentTagFeature = getParentTagFeatureColumns(tagTableDetail);
|
|
969
982
|
const singleIdentityColumn = getSingleTagIdentityColumn(tagTableDetail);
|
|
970
983
|
const rows = db
|
|
971
984
|
.prepare(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
985
|
+
[
|
|
986
|
+
selectionPrefix,
|
|
987
|
+
"FROM",
|
|
988
|
+
quoteIdentifier(tagTableDetail.name),
|
|
989
|
+
"ORDER BY",
|
|
990
|
+
orderByColumns.map((column) => quoteIdentifier(column)).join(", "),
|
|
991
|
+
"ASC",
|
|
992
|
+
].join(" ")
|
|
975
993
|
)
|
|
976
994
|
.all();
|
|
977
995
|
|
|
@@ -1171,7 +1189,13 @@ class MediaTaggingService {
|
|
|
1171
1189
|
|
|
1172
1190
|
loadCurrentMediaRow(db, untaggedQuery, identityColumns, skippedMediaKeys = []) {
|
|
1173
1191
|
const exclusion = buildExcludedKeyClause(identityColumns, skippedMediaKeys);
|
|
1174
|
-
const sqlParts = [
|
|
1192
|
+
const sqlParts = [
|
|
1193
|
+
[
|
|
1194
|
+
"SELECT * FROM (",
|
|
1195
|
+
buildWrappedQuery(untaggedQuery),
|
|
1196
|
+
") AS media_tagging_source",
|
|
1197
|
+
].join(""),
|
|
1198
|
+
];
|
|
1175
1199
|
|
|
1176
1200
|
if (exclusion.sql) {
|
|
1177
1201
|
sqlParts.push(`WHERE NOT (${exclusion.sql})`);
|
|
@@ -1188,13 +1212,20 @@ class MediaTaggingService {
|
|
|
1188
1212
|
const whereClause = buildKeyWhereClause(identityColumns, identityValues);
|
|
1189
1213
|
const selectionPrefix =
|
|
1190
1214
|
mediaTableDetail.identityStrategy?.type === "rowid"
|
|
1191
|
-
?
|
|
1215
|
+
? ["SELECT rowid AS", quoteIdentifier(MEDIA_ROWID_ALIAS), ", *"].join(" ")
|
|
1192
1216
|
: "SELECT *";
|
|
1193
1217
|
|
|
1194
1218
|
return (
|
|
1195
1219
|
db
|
|
1196
1220
|
.prepare(
|
|
1197
|
-
|
|
1221
|
+
[
|
|
1222
|
+
selectionPrefix,
|
|
1223
|
+
"FROM",
|
|
1224
|
+
quoteIdentifier(mediaTableDetail.name),
|
|
1225
|
+
"WHERE",
|
|
1226
|
+
whereClause.sql,
|
|
1227
|
+
"LIMIT 1",
|
|
1228
|
+
].join(" ")
|
|
1198
1229
|
)
|
|
1199
1230
|
.get(...whereClause.params) ?? null
|
|
1200
1231
|
);
|
|
@@ -1209,7 +1240,14 @@ class MediaTaggingService {
|
|
|
1209
1240
|
.join(", ");
|
|
1210
1241
|
const rows = db
|
|
1211
1242
|
.prepare(
|
|
1212
|
-
|
|
1243
|
+
[
|
|
1244
|
+
"SELECT",
|
|
1245
|
+
tagSelection,
|
|
1246
|
+
"FROM",
|
|
1247
|
+
quoteIdentifier(selectedMapping.tableName),
|
|
1248
|
+
"WHERE",
|
|
1249
|
+
mediaWhere,
|
|
1250
|
+
].join(" ")
|
|
1213
1251
|
)
|
|
1214
1252
|
.all(
|
|
1215
1253
|
selectedMapping.mediaForeignKey.mappings.map((mapping) => currentRow[mapping.to] ?? null)
|
|
@@ -1247,16 +1285,30 @@ class MediaTaggingService {
|
|
|
1247
1285
|
};
|
|
1248
1286
|
}
|
|
1249
1287
|
|
|
1250
|
-
|
|
1288
|
+
let resolvedPath = null;
|
|
1289
|
+
|
|
1290
|
+
try {
|
|
1291
|
+
resolvedPath = this.resolveMediaFilePath(normalizedPath, connection);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (!(error instanceof ValidationError)) {
|
|
1294
|
+
throw error;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1251
1297
|
|
|
1252
1298
|
return {
|
|
1253
1299
|
previewKind: inferPreviewKind(normalizedPath),
|
|
1254
|
-
previewUrl:
|
|
1300
|
+
previewUrl: resolvedPath
|
|
1301
|
+
? `/api/media-tagging/media-file?path=${encodeURIComponent(normalizedPath)}`
|
|
1302
|
+
: null,
|
|
1255
1303
|
resolvedPath,
|
|
1256
1304
|
existsOnDisk: Boolean(resolvedPath && fs.existsSync(resolvedPath)),
|
|
1257
1305
|
};
|
|
1258
1306
|
}
|
|
1259
1307
|
|
|
1308
|
+
getMediaBaseDirectory(connection = this.getActiveConnection()) {
|
|
1309
|
+
return connection?.path ? path.dirname(connection.path) : null;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1260
1312
|
resolveMediaFilePath(rawPath, connection = this.getActiveConnection()) {
|
|
1261
1313
|
const normalizedPath = String(rawPath ?? "").trim();
|
|
1262
1314
|
|
|
@@ -1264,16 +1316,46 @@ class MediaTaggingService {
|
|
|
1264
1316
|
return null;
|
|
1265
1317
|
}
|
|
1266
1318
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1319
|
+
const mediaBaseDirectory = this.getMediaBaseDirectory(connection);
|
|
1320
|
+
|
|
1321
|
+
if (!mediaBaseDirectory) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const candidatePath = /^file:\/\//i.test(normalizedPath)
|
|
1326
|
+
? fileURLToPath(normalizedPath)
|
|
1327
|
+
: normalizedPath;
|
|
1328
|
+
|
|
1329
|
+
return resolvePathInsideDirectory(
|
|
1330
|
+
mediaBaseDirectory,
|
|
1331
|
+
candidatePath,
|
|
1332
|
+
"Media file path"
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
getMediaFileForRequest(rawPath) {
|
|
1337
|
+
const normalizedPath = String(rawPath ?? "").trim();
|
|
1338
|
+
const resolvedPath = this.resolveMediaFilePath(normalizedPath);
|
|
1339
|
+
|
|
1340
|
+
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
1341
|
+
throw new NotFoundError(`Media file not found: ${normalizedPath}`);
|
|
1269
1342
|
}
|
|
1270
1343
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1344
|
+
const stat = fs.statSync(resolvedPath);
|
|
1345
|
+
|
|
1346
|
+
if (!stat.isFile()) {
|
|
1347
|
+
throw new NotFoundError(`Media file not found: ${normalizedPath}`);
|
|
1273
1348
|
}
|
|
1274
1349
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1350
|
+
return {
|
|
1351
|
+
directory: path.dirname(resolvedPath),
|
|
1352
|
+
fileName: path.basename(resolvedPath),
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
sendMediaFile(rawPath, res) {
|
|
1357
|
+
const mediaFile = this.getMediaFileForRequest(rawPath);
|
|
1358
|
+
res.sendFile(mediaFile.fileName, { root: mediaFile.directory });
|
|
1277
1359
|
}
|
|
1278
1360
|
|
|
1279
1361
|
createTag(payload = {}) {
|
|
@@ -1329,9 +1411,13 @@ class MediaTaggingService {
|
|
|
1329
1411
|
|
|
1330
1412
|
const parentTag = db
|
|
1331
1413
|
.prepare(
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1414
|
+
[
|
|
1415
|
+
"SELECT * FROM",
|
|
1416
|
+
quoteIdentifier(tagTableDetail.name),
|
|
1417
|
+
"WHERE",
|
|
1418
|
+
quoteIdentifier(singleIdentityColumn),
|
|
1419
|
+
"= ? LIMIT 1",
|
|
1420
|
+
].join(" ")
|
|
1335
1421
|
)
|
|
1336
1422
|
.get(normalizedValues[parentIdColumnName]);
|
|
1337
1423
|
|
|
@@ -1353,12 +1439,13 @@ class MediaTaggingService {
|
|
|
1353
1439
|
throw new ValidationError("Enter at least one tag value before saving.");
|
|
1354
1440
|
}
|
|
1355
1441
|
|
|
1356
|
-
const sql =
|
|
1357
|
-
INSERT INTO
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
VALUES
|
|
1361
|
-
|
|
1442
|
+
const sql = [
|
|
1443
|
+
"INSERT INTO",
|
|
1444
|
+
quoteIdentifier(tagTableDetail.name),
|
|
1445
|
+
"(" + providedColumns.map((column) => quoteIdentifier(column.name)).join(", ") + ")",
|
|
1446
|
+
"VALUES",
|
|
1447
|
+
"(" + providedColumns.map(() => "?").join(", ") + ")",
|
|
1448
|
+
].join(" ");
|
|
1362
1449
|
|
|
1363
1450
|
const providedColumnNames = providedColumns.map((column) => column.name);
|
|
1364
1451
|
const insertTag = db.transaction(() => {
|
|
@@ -1373,12 +1460,15 @@ class MediaTaggingService {
|
|
|
1373
1460
|
// the tag still ends up with a stable key for selection and mapping.
|
|
1374
1461
|
if (implicitPrimaryKey && !providedColumnNames.includes(implicitPrimaryKey.name)) {
|
|
1375
1462
|
db.prepare(
|
|
1376
|
-
|
|
1377
|
-
UPDATE
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1463
|
+
[
|
|
1464
|
+
"UPDATE",
|
|
1465
|
+
quoteIdentifier(tagTableDetail.name),
|
|
1466
|
+
"SET",
|
|
1467
|
+
quoteIdentifier(implicitPrimaryKey.name),
|
|
1468
|
+
"= ? WHERE rowid = ? AND",
|
|
1469
|
+
quoteIdentifier(implicitPrimaryKey.name),
|
|
1470
|
+
"IS NULL",
|
|
1471
|
+
].join(" ")
|
|
1382
1472
|
).run(result.lastInsertRowid, result.lastInsertRowid);
|
|
1383
1473
|
}
|
|
1384
1474
|
});
|
|
@@ -1426,13 +1516,20 @@ class MediaTaggingService {
|
|
|
1426
1516
|
const mappingWhere = buildKeyWhereClause(mappingColumns, keyValues);
|
|
1427
1517
|
|
|
1428
1518
|
db.prepare(
|
|
1429
|
-
|
|
1519
|
+
[
|
|
1520
|
+
"DELETE FROM",
|
|
1521
|
+
quoteIdentifier(selectedMapping.tableName),
|
|
1522
|
+
"WHERE",
|
|
1523
|
+
mappingWhere.sql,
|
|
1524
|
+
].join(" ")
|
|
1430
1525
|
).run(mappingWhere.params);
|
|
1431
1526
|
}
|
|
1432
1527
|
|
|
1433
1528
|
const tagWhere = buildKeyWhereClause(keyColumns, keyValues);
|
|
1434
1529
|
const result = db
|
|
1435
|
-
.prepare(
|
|
1530
|
+
.prepare(
|
|
1531
|
+
["DELETE FROM", quoteIdentifier(tagTableDetail.name), "WHERE", tagWhere.sql].join(" ")
|
|
1532
|
+
)
|
|
1436
1533
|
.run(tagWhere.params);
|
|
1437
1534
|
|
|
1438
1535
|
if (!result.changes) {
|
|
@@ -1548,12 +1645,13 @@ class MediaTaggingService {
|
|
|
1548
1645
|
|
|
1549
1646
|
if (!this.mappingExists(db, selectedMapping.tableName, mappingPayload)) {
|
|
1550
1647
|
const columns = Object.keys(mappingPayload);
|
|
1551
|
-
const insertSql =
|
|
1552
|
-
INSERT INTO
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
VALUES
|
|
1556
|
-
|
|
1648
|
+
const insertSql = [
|
|
1649
|
+
"INSERT INTO",
|
|
1650
|
+
quoteIdentifier(selectedMapping.tableName),
|
|
1651
|
+
"(" + columns.map((column) => quoteIdentifier(column)).join(", ") + ")",
|
|
1652
|
+
"VALUES",
|
|
1653
|
+
"(" + columns.map(() => "?").join(", ") + ")",
|
|
1654
|
+
].join(" ");
|
|
1557
1655
|
|
|
1558
1656
|
db.prepare(insertSql).run(columns.map((column) => mappingPayload[column]));
|
|
1559
1657
|
}
|
|
@@ -1637,7 +1735,13 @@ class MediaTaggingService {
|
|
|
1637
1735
|
const whereClause = columns.map((column) => `${quoteIdentifier(column)} = ?`).join(" AND ");
|
|
1638
1736
|
const row = db
|
|
1639
1737
|
.prepare(
|
|
1640
|
-
|
|
1738
|
+
[
|
|
1739
|
+
"SELECT 1 AS exists_flag FROM",
|
|
1740
|
+
quoteIdentifier(mappingTableName),
|
|
1741
|
+
"WHERE",
|
|
1742
|
+
whereClause,
|
|
1743
|
+
"LIMIT 1",
|
|
1744
|
+
].join(" ")
|
|
1641
1745
|
)
|
|
1642
1746
|
.get(columns.map((column) => mappingPayload[column]));
|
|
1643
1747
|
|
|
@@ -1654,9 +1758,14 @@ class MediaTaggingService {
|
|
|
1654
1758
|
const values = identity.columns.map((column) => currentRow[column] ?? null);
|
|
1655
1759
|
|
|
1656
1760
|
db.prepare(
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1761
|
+
[
|
|
1762
|
+
"UPDATE",
|
|
1763
|
+
quoteIdentifier(mediaTableDetail.name),
|
|
1764
|
+
"SET",
|
|
1765
|
+
quoteIdentifier(taggedColumn),
|
|
1766
|
+
"= 1 WHERE",
|
|
1767
|
+
whereClause,
|
|
1768
|
+
].join(" ")
|
|
1660
1769
|
).run(values);
|
|
1661
1770
|
return;
|
|
1662
1771
|
}
|
|
@@ -1671,9 +1780,13 @@ class MediaTaggingService {
|
|
|
1671
1780
|
}
|
|
1672
1781
|
|
|
1673
1782
|
db.prepare(
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1783
|
+
[
|
|
1784
|
+
"UPDATE",
|
|
1785
|
+
quoteIdentifier(mediaTableDetail.name),
|
|
1786
|
+
"SET",
|
|
1787
|
+
quoteIdentifier(taggedColumn),
|
|
1788
|
+
"= 1 WHERE rowid = ?",
|
|
1789
|
+
].join(" ")
|
|
1677
1790
|
).run(rowIdValue);
|
|
1678
1791
|
return;
|
|
1679
1792
|
}
|
|
@@ -54,9 +54,9 @@ class StructureService {
|
|
|
54
54
|
const table = getTableDetail(db, tableName);
|
|
55
55
|
const previewLimit = Math.max(1, this.appStateStore.getSettings().defaultPageSize ?? 50);
|
|
56
56
|
const previewStatement = db.prepare(
|
|
57
|
-
|
|
57
|
+
["SELECT * FROM", quoteIdentifier(tableName), "LIMIT ?"].join(" ")
|
|
58
58
|
);
|
|
59
|
-
const previewRows = serializeRows(previewStatement.all());
|
|
59
|
+
const previewRows = serializeRows(previewStatement.all(previewLimit));
|
|
60
60
|
const previewColumns = previewStatement.columns().map((column) => column.name);
|
|
61
61
|
|
|
62
62
|
return {
|
|
@@ -42,16 +42,32 @@ function buildCreateTableSql(draft) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function buildAlterTableRenameSql(fromName, toName) {
|
|
45
|
-
return
|
|
45
|
+
return [
|
|
46
|
+
"ALTER TABLE",
|
|
47
|
+
quoteIdentifier(fromName),
|
|
48
|
+
"RENAME TO",
|
|
49
|
+
quoteIdentifier(toName),
|
|
50
|
+
].join(" ") + ";";
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function buildAlterTableAddColumnSql(tableName, column) {
|
|
49
|
-
return
|
|
54
|
+
return [
|
|
55
|
+
"ALTER TABLE",
|
|
56
|
+
quoteIdentifier(tableName),
|
|
57
|
+
"ADD COLUMN",
|
|
58
|
+
buildColumnDefinition(column),
|
|
59
|
+
].join(" ") + ";";
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function buildInsertRowsSql(tableName, columns) {
|
|
53
63
|
const placeholders = columns.map(() => "?").join(", ");
|
|
54
|
-
return
|
|
64
|
+
return [
|
|
65
|
+
"INSERT INTO",
|
|
66
|
+
quoteIdentifier(tableName),
|
|
67
|
+
"(" + quoteIdentifierList(columns) + ")",
|
|
68
|
+
"VALUES",
|
|
69
|
+
"(" + placeholders + ");",
|
|
70
|
+
].join(" ");
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
module.exports = {
|