@restura/core 0.1.0-alpha.11 → 0.1.0-alpha.13

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/dist/index.mjs CHANGED
@@ -1204,6 +1204,10 @@ function convertTable(table) {
1204
1204
  // src/restura/sql/PsqlEngine.ts
1205
1205
  import { ObjectUtils as ObjectUtils4 } from "@redskytech/core-utils";
1206
1206
 
1207
+ // src/restura/sql/PsqlPool.ts
1208
+ import pg from "pg";
1209
+ import format3 from "pg-format";
1210
+
1207
1211
  // src/restura/sql/PsqlUtils.ts
1208
1212
  import format2 from "pg-format";
1209
1213
  function escapeColumnName(columnName) {
@@ -1239,7 +1243,9 @@ function isValueNumber2(value) {
1239
1243
  function SQL(strings, ...values) {
1240
1244
  let query = strings[0];
1241
1245
  values.forEach((value, index) => {
1242
- if (isValueNumber2(value)) {
1246
+ if (typeof value === "boolean") {
1247
+ query += value;
1248
+ } else if (typeof value === "number") {
1243
1249
  query += value;
1244
1250
  } else {
1245
1251
  query += format2.literal(value);
@@ -1249,6 +1255,76 @@ function SQL(strings, ...values) {
1249
1255
  return query;
1250
1256
  }
1251
1257
 
1258
+ // src/restura/sql/PsqlPool.ts
1259
+ var { Pool } = pg;
1260
+ var PsqlPool = class {
1261
+ constructor(poolConfig) {
1262
+ this.poolConfig = poolConfig;
1263
+ this.pool = new Pool(poolConfig);
1264
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1265
+ logger.info("Connected to PostgreSQL database");
1266
+ }).catch((error) => {
1267
+ logger.error("Error connecting to database", error);
1268
+ process.exit(1);
1269
+ });
1270
+ }
1271
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1272
+ async queryOne(query, options, requesterDetails) {
1273
+ const formattedQuery = questionMarksToOrderedParams(query);
1274
+ this.logSqlStatement(formattedQuery, options, requesterDetails);
1275
+ try {
1276
+ const response = await this.pool.query(formattedQuery, options);
1277
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1278
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1279
+ return response.rows[0];
1280
+ } catch (error) {
1281
+ console.error(error, query, options);
1282
+ if (RsError.isRsError(error)) throw error;
1283
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1284
+ throw new RsError("DUPLICATE", error.message);
1285
+ }
1286
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1287
+ }
1288
+ }
1289
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1290
+ async runQuery(query, options, requesterDetails) {
1291
+ const formattedQuery = questionMarksToOrderedParams(query);
1292
+ this.logSqlStatement(formattedQuery, options, requesterDetails);
1293
+ const queryUpdated = query.replace(/[\t\n]/g, " ");
1294
+ console.log(queryUpdated, options);
1295
+ try {
1296
+ const response = await this.pool.query(formattedQuery, options);
1297
+ return response.rows;
1298
+ } catch (error) {
1299
+ console.error(error, query, options);
1300
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1301
+ throw new RsError("DUPLICATE", error.message);
1302
+ }
1303
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1304
+ }
1305
+ }
1306
+ logSqlStatement(query, options, requesterDetails, prefix = "") {
1307
+ if (logger.level !== "silly") return;
1308
+ let sqlStatement = "";
1309
+ if (options.length === 0) {
1310
+ sqlStatement = query;
1311
+ } else {
1312
+ let stringIndex = 0;
1313
+ sqlStatement = query.replace(/\$\d+/g, () => {
1314
+ const value = options[stringIndex++];
1315
+ if (typeof value === "number") return value.toString();
1316
+ return format3.literal(value);
1317
+ });
1318
+ }
1319
+ let initiator = "Anonymous";
1320
+ if ("userId" in requesterDetails && requesterDetails.userId)
1321
+ initiator = `User Id (${requesterDetails.userId.toString()})`;
1322
+ if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1323
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1324
+ ${sqlStatement}`);
1325
+ }
1326
+ };
1327
+
1252
1328
  // src/restura/sql/SqlEngine.ts
1253
1329
  import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
1254
1330
  var SqlEngine = class {
@@ -1353,6 +1429,57 @@ var SqlEngine = class {
1353
1429
  // src/restura/sql/filterPsqlParser.ts
1354
1430
  import peg from "pegjs";
1355
1431
  var filterSqlGrammar = `
1432
+ {
1433
+ // ported from pg-format but intentionally will add double quotes to every column
1434
+ function quoteSqlIdentity(value) {
1435
+ if (value === undefined || value === null) {
1436
+ throw new Error('SQL identifier cannot be null or undefined');
1437
+ } else if (value === false) {
1438
+ return '"f"';
1439
+ } else if (value === true) {
1440
+ return '"t"';
1441
+ } else if (value instanceof Date) {
1442
+ // return '"' + formatDate(value.toISOString()) + '"';
1443
+ } else if (value instanceof Buffer) {
1444
+ throw new Error('SQL identifier cannot be a buffer');
1445
+ } else if (Array.isArray(value) === true) {
1446
+ var temp = [];
1447
+ for (var i = 0; i < value.length; i++) {
1448
+ if (Array.isArray(value[i]) === true) {
1449
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1450
+ } else {
1451
+ // temp.push(quoteIdent(value[i]));
1452
+ }
1453
+ }
1454
+ return temp.toString();
1455
+ } else if (value === Object(value)) {
1456
+ throw new Error('SQL identifier cannot be an object');
1457
+ }
1458
+
1459
+ var ident = value.toString().slice(0); // create copy
1460
+
1461
+ // do not quote a valid, unquoted identifier
1462
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1463
+ // return ident;
1464
+ // }
1465
+
1466
+ var quoted = '"';
1467
+
1468
+ for (var i = 0; i < ident.length; i++) {
1469
+ var c = ident[i];
1470
+ if (c === '"') {
1471
+ quoted += c + c;
1472
+ } else {
1473
+ quoted += c;
1474
+ }
1475
+ }
1476
+
1477
+ quoted += '"';
1478
+
1479
+ return quoted;
1480
+ };
1481
+ }
1482
+
1356
1483
  start = expressionList
1357
1484
 
1358
1485
  expressionList =
@@ -1371,9 +1498,9 @@ negate = "!"
1371
1498
  operator = "and"i / "or"i
1372
1499
 
1373
1500
 
1374
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1501
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1375
1502
  /
1376
- text:text { return format.ident(text); }
1503
+ text:text { return quoteSqlIdentity(text); }
1377
1504
 
1378
1505
 
1379
1506
  text = text:[a-z0-9-_:@]i+ { return text.join("");}
@@ -1399,18 +1526,155 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
1399
1526
  var filterPsqlParser_default = filterPsqlParser;
1400
1527
 
1401
1528
  // src/restura/sql/PsqlEngine.ts
1529
+ import getDiff from "@wmfs/pg-diff-sync";
1530
+ import pgInfo from "@wmfs/pg-info";
1531
+ var { Client } = "pg";
1532
+ var systemUser = {
1533
+ role: "",
1534
+ host: "",
1535
+ ipAddress: "",
1536
+ isSystemUser: true
1537
+ };
1402
1538
  var PsqlEngine = class extends SqlEngine {
1403
1539
  constructor(psqlConnectionPool) {
1404
1540
  super();
1405
1541
  this.psqlConnectionPool = psqlConnectionPool;
1406
1542
  }
1407
- async diffDatabaseToSchema(schema) {
1408
- console.log(schema);
1409
- return Promise.resolve("");
1543
+ async createDatabaseFromSchema(schema, connection) {
1544
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1545
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1546
+ return sqlFullStatement;
1410
1547
  }
1411
1548
  generateDatabaseSchemaFromSchema(schema) {
1412
- console.log(schema);
1413
- return "";
1549
+ const sqlStatements = [];
1550
+ const enums = [];
1551
+ const indexes = [];
1552
+ for (const table of schema.database) {
1553
+ let sql = `CREATE TABLE "${table.name}"
1554
+ ( `;
1555
+ const tableColumns = [];
1556
+ for (const column of table.columns) {
1557
+ let columnSql = "";
1558
+ if (column.type === "ENUM") {
1559
+ enums.push(`CREATE TYPE ${schemaToPsqlType(column, table.name)} AS ENUM (${column.value});`);
1560
+ }
1561
+ columnSql += ` "${column.name}" ${schemaToPsqlType(column, table.name)}`;
1562
+ let value = column.value;
1563
+ if (column.type === "JSON") value = "";
1564
+ if (column.type === "JSONB") value = "";
1565
+ if (column.type === "DECIMAL" && value) {
1566
+ value = value.replace("-", ",").replace(/['"]/g, "");
1567
+ }
1568
+ if (value && column.type !== "ENUM") {
1569
+ columnSql += `(${value})`;
1570
+ } else if (column.length) columnSql += `(${column.length})`;
1571
+ if (column.isPrimary) {
1572
+ columnSql += " PRIMARY KEY ";
1573
+ }
1574
+ if (column.isUnique) {
1575
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
1576
+ }
1577
+ if (column.isNullable) columnSql += " NULL";
1578
+ else columnSql += " NOT NULL";
1579
+ if (column.default) columnSql += ` DEFAULT '${column.default}'`;
1580
+ tableColumns.push(columnSql);
1581
+ }
1582
+ sql += tableColumns.join(", \n");
1583
+ for (const index of table.indexes) {
1584
+ if (!index.isPrimaryKey) {
1585
+ let unique = " ";
1586
+ if (index.isUnique) unique = "UNIQUE ";
1587
+ indexes.push(
1588
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
1589
+ return `"${item}" ${index.order}`;
1590
+ }).join(", ")})`
1591
+ );
1592
+ }
1593
+ }
1594
+ sql += "\n);";
1595
+ sqlStatements.push(sql);
1596
+ }
1597
+ for (const table of schema.database) {
1598
+ if (!table.foreignKeys.length) continue;
1599
+ const sql = `ALTER TABLE "${table.name}" `;
1600
+ const constraints = [];
1601
+ for (const foreignKey of table.foreignKeys) {
1602
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
1603
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
1604
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
1605
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
1606
+ constraints.push(constraint);
1607
+ }
1608
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
1609
+ }
1610
+ for (const table of schema.database) {
1611
+ if (!table.checkConstraints.length) continue;
1612
+ const sql = `ALTER TABLE "${table.name}" `;
1613
+ const constraints = [];
1614
+ for (const check of table.checkConstraints) {
1615
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
1616
+ constraints.push(constraint);
1617
+ }
1618
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
1619
+ }
1620
+ sqlStatements.push(indexes.join(";\n"));
1621
+ return enums.join("\n") + "\n" + sqlStatements.join("\n\n");
1622
+ }
1623
+ async getScratchPool() {
1624
+ await this.psqlConnectionPool.runQuery(
1625
+ `DROP DATABASE IF EXISTS ${this.psqlConnectionPool.poolConfig.database}_scratch`,
1626
+ [],
1627
+ systemUser
1628
+ );
1629
+ await this.psqlConnectionPool.runQuery(
1630
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
1631
+ [],
1632
+ systemUser
1633
+ );
1634
+ const scratchPool = new PsqlPool({
1635
+ host: this.psqlConnectionPool.poolConfig.host,
1636
+ port: this.psqlConnectionPool.poolConfig.port,
1637
+ user: this.psqlConnectionPool.poolConfig.user,
1638
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
1639
+ password: this.psqlConnectionPool.poolConfig.password,
1640
+ max: this.psqlConnectionPool.poolConfig.max,
1641
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
1642
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1643
+ });
1644
+ return scratchPool;
1645
+ }
1646
+ async diffDatabaseToSchema(schema) {
1647
+ const scratchPool = await this.getScratchPool();
1648
+ await this.createDatabaseFromSchema(schema, scratchPool);
1649
+ const originalClient = new Client({
1650
+ database: this.psqlConnectionPool.poolConfig.database,
1651
+ user: this.psqlConnectionPool.poolConfig.user,
1652
+ password: this.psqlConnectionPool.poolConfig.password,
1653
+ host: this.psqlConnectionPool.poolConfig.host,
1654
+ port: this.psqlConnectionPool.poolConfig.port
1655
+ });
1656
+ const scratchClient = new Client({
1657
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
1658
+ user: this.psqlConnectionPool.poolConfig.user,
1659
+ password: this.psqlConnectionPool.poolConfig.password,
1660
+ host: this.psqlConnectionPool.poolConfig.host,
1661
+ port: this.psqlConnectionPool.poolConfig.port
1662
+ });
1663
+ await originalClient.connect();
1664
+ await scratchClient.connect();
1665
+ const info1 = await pgInfo({
1666
+ client: originalClient,
1667
+ schema: "public"
1668
+ });
1669
+ const info2 = await pgInfo({
1670
+ client: scratchClient,
1671
+ schema: "public"
1672
+ });
1673
+ const diff = getDiff(info1, info2);
1674
+ console.log("Schema differences:", diff);
1675
+ await originalClient.end();
1676
+ await scratchClient.end();
1677
+ return diff.join("\n");
1414
1678
  }
1415
1679
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1416
1680
  if (!item.subquery) return "";
@@ -1521,22 +1785,20 @@ var PsqlEngine = class extends SqlEngine {
1521
1785
  );
1522
1786
  } else if (routeData.type === "PAGED") {
1523
1787
  const data = req.data;
1524
- const pageResults = await this.psqlConnectionPool.runQuery(
1525
- `${selectStatement}${sqlStatement}${groupByOrderByStatement} LIMIT ? OFFSET ?;SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
1526
- ${sqlStatement};`,
1527
- [
1528
- ...sqlParams,
1529
- data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER,
1530
- (data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER,
1531
- ...sqlParams
1532
- ],
1788
+ const pagePromise = this.psqlConnectionPool.runQuery(
1789
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
1790
+ sqlParams,
1533
1791
  req.requesterDetails
1534
1792
  );
1793
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
1794
+ ${sqlStatement};`;
1795
+ const totalPromise = await this.psqlConnectionPool.runQuery(totalQuery, sqlParams, req.requesterDetails);
1796
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
1535
1797
  let total = 0;
1536
- if (ObjectUtils4.isArrayWithData(pageResults)) {
1537
- total = pageResults[1][0].total;
1798
+ if (ObjectUtils4.isArrayWithData(totalResponse)) {
1799
+ total = totalResponse[0].total;
1538
1800
  }
1539
- return { data: pageResults[0], total };
1801
+ return { data: pageResults, total };
1540
1802
  } else {
1541
1803
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
1542
1804
  }
@@ -1686,77 +1948,12 @@ ${sqlStatement};`,
1686
1948
  return whereClause;
1687
1949
  }
1688
1950
  };
1689
-
1690
- // src/restura/sql/PsqlPool.ts
1691
- import pg from "pg";
1692
- import format3 from "pg-format";
1693
- var { Pool } = pg;
1694
- var PsqlPool = class {
1695
- constructor(poolConfig) {
1696
- this.poolConfig = poolConfig;
1697
- this.pool = new Pool(poolConfig);
1698
- this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1699
- logger.info("Connected to PostgreSQL database");
1700
- }).catch((error) => {
1701
- logger.error("Error connecting to database", error);
1702
- process.exit(1);
1703
- });
1704
- }
1705
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1706
- async queryOne(query, options, requesterDetails) {
1707
- const formattedQuery = questionMarksToOrderedParams(query);
1708
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1709
- try {
1710
- const response = await this.pool.query(formattedQuery, options);
1711
- if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1712
- else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1713
- return response.rows[0];
1714
- } catch (error) {
1715
- console.error(error, query, options);
1716
- if (RsError.isRsError(error)) throw error;
1717
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1718
- throw new RsError("DUPLICATE", error.message);
1719
- }
1720
- throw new RsError("DATABASE_ERROR", `${error.message}`);
1721
- }
1722
- }
1723
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1724
- async runQuery(query, options, requesterDetails) {
1725
- const formattedQuery = questionMarksToOrderedParams(query);
1726
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1727
- const queryUpdated = query.replace(/[\t\n]/g, " ");
1728
- console.log(queryUpdated, options);
1729
- try {
1730
- const response = await this.pool.query(formattedQuery, options);
1731
- return response.rows;
1732
- } catch (error) {
1733
- console.error(error, query, options);
1734
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1735
- throw new RsError("DUPLICATE", error.message);
1736
- }
1737
- throw new RsError("DATABASE_ERROR", `${error.message}`);
1738
- }
1739
- }
1740
- logSqlStatement(query, options, requesterDetails, prefix = "") {
1741
- if (logger.level !== "silly") return;
1742
- let sqlStatement = "";
1743
- if (options.length === 0) {
1744
- sqlStatement = query;
1745
- } else {
1746
- let stringIndex = 0;
1747
- sqlStatement = query.replace(/\$\d+/g, () => {
1748
- const value = options[stringIndex++];
1749
- if (typeof value === "number") return value.toString();
1750
- return format3.literal(value);
1751
- });
1752
- }
1753
- let initiator = "Anonymous";
1754
- if ("userId" in requesterDetails && requesterDetails.userId)
1755
- initiator = `User Id (${requesterDetails.userId.toString()})`;
1756
- if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1757
- logger.silly(`${prefix}query by ${initiator}, Query ->
1758
- ${sqlStatement}`);
1759
- }
1951
+ var schemaToPsqlType = (column, tableName) => {
1952
+ if (column.hasAutoIncrement) return "BIGSERIAL";
1953
+ if (column.type === "ENUM") return `"${tableName}_${column.name}_enum"`;
1954
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
1955
+ if (column.type === "MEDIUMINT") return "INT";
1956
+ return column.type;
1760
1957
  };
1761
1958
 
1762
1959
  // src/restura/restura.ts