@restura/core 0.1.0-alpha.10 → 0.1.0-alpha.12

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.js CHANGED
@@ -258,7 +258,7 @@ var import_crypto = require("crypto");
258
258
  var express = __toESM(require("express"));
259
259
  var import_fs3 = __toESM(require("fs"));
260
260
  var import_path3 = __toESM(require("path"));
261
- var import_pg2 = __toESM(require("pg"));
261
+ var import_pg3 = __toESM(require("pg"));
262
262
  var prettier3 = __toESM(require("prettier"));
263
263
 
264
264
  // src/restura/sql/SqlUtils.ts
@@ -536,7 +536,7 @@ var ApiTree = class _ApiTree {
536
536
  break;
537
537
  }
538
538
  }
539
- return `'${p.name}'${p.required ? "" : "?"}:${requestType}`;
539
+ return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
540
540
  }).join(";\n")}${import_core_utils.ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
541
541
  `;
542
542
  modelString += `}`;
@@ -560,18 +560,18 @@ var ApiTree = class _ApiTree {
560
560
  return nested;
561
561
  }
562
562
  getNameAndType(p) {
563
- let responseType = "any", optional = false, array = false;
563
+ let responseType = "any", isNullable = false, array = false;
564
564
  if (p.selector) {
565
- ({ responseType, optional } = this.getTypeFromTable(p.selector, p.name));
565
+ ({ responseType, isNullable } = this.getTypeFromTable(p.selector, p.name));
566
566
  } else if (p.subquery) {
567
567
  responseType = this.getFields(p.subquery.properties);
568
568
  array = true;
569
569
  }
570
- return `${p.name}${optional ? "?" : ""}:${responseType}${array ? "[]" : ""}`;
570
+ return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
571
571
  }
572
572
  getTypeFromTable(selector, name) {
573
573
  const path4 = selector.split(".");
574
- if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { responseType: "any", optional: false };
574
+ if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { responseType: "any", isNullable: false };
575
575
  let tableName = path4.length == 2 ? path4[0] : name;
576
576
  const columnName = path4.length == 2 ? path4[1] : path4[0];
577
577
  let table = this.database.find((t) => t.name == tableName);
@@ -581,10 +581,10 @@ var ApiTree = class _ApiTree {
581
581
  table = this.database.find((t) => t.name == tableName);
582
582
  }
583
583
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
584
- if (!table || !column) return { responseType: "any", optional: false };
584
+ if (!table || !column) return { responseType: "any", isNullable: false };
585
585
  return {
586
586
  responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
587
- optional: column.roles.length > 0 || column.isNullable
587
+ isNullable: column.roles.length > 0 || column.isNullable
588
588
  };
589
589
  }
590
590
  };
@@ -790,6 +790,7 @@ var joinDataSchema = import_zod3.z.object({
790
790
  var requestDataSchema = import_zod3.z.object({
791
791
  name: import_zod3.z.string(),
792
792
  required: import_zod3.z.boolean(),
793
+ isNullable: import_zod3.z.boolean().optional().default(false),
793
794
  validator: import_zod3.z.array(validatorDataSchema)
794
795
  }).strict();
795
796
  var responseDataSchema = import_zod3.z.object({
@@ -1065,6 +1066,7 @@ function validateRequestParams(req, routeData, validationSchema) {
1065
1066
  });
1066
1067
  }
1067
1068
  function validateRequestSingleParam(requestValue, requestParam) {
1069
+ if (requestParam.isNullable && requestValue === null) return;
1068
1070
  requestParam.validator.forEach((validator) => {
1069
1071
  switch (validator.type) {
1070
1072
  case "TYPE_CHECK":
@@ -1244,6 +1246,10 @@ function convertTable(table) {
1244
1246
  // src/restura/sql/PsqlEngine.ts
1245
1247
  var import_core_utils5 = require("@redskytech/core-utils");
1246
1248
 
1249
+ // src/restura/sql/PsqlPool.ts
1250
+ var import_pg = __toESM(require("pg"));
1251
+ var import_pg_format2 = __toESM(require("pg-format"));
1252
+
1247
1253
  // src/restura/sql/PsqlUtils.ts
1248
1254
  var import_pg_format = __toESM(require("pg-format"));
1249
1255
  function escapeColumnName(columnName) {
@@ -1279,7 +1285,9 @@ function isValueNumber2(value) {
1279
1285
  function SQL(strings, ...values) {
1280
1286
  let query = strings[0];
1281
1287
  values.forEach((value, index) => {
1282
- if (isValueNumber2(value)) {
1288
+ if (typeof value === "boolean") {
1289
+ query += value;
1290
+ } else if (typeof value === "number") {
1283
1291
  query += value;
1284
1292
  } else {
1285
1293
  query += import_pg_format.default.literal(value);
@@ -1289,6 +1297,76 @@ function SQL(strings, ...values) {
1289
1297
  return query;
1290
1298
  }
1291
1299
 
1300
+ // src/restura/sql/PsqlPool.ts
1301
+ var { Pool } = import_pg.default;
1302
+ var PsqlPool = class {
1303
+ constructor(poolConfig) {
1304
+ this.poolConfig = poolConfig;
1305
+ this.pool = new Pool(poolConfig);
1306
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1307
+ logger.info("Connected to PostgreSQL database");
1308
+ }).catch((error) => {
1309
+ logger.error("Error connecting to database", error);
1310
+ process.exit(1);
1311
+ });
1312
+ }
1313
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1314
+ async queryOne(query, options, requesterDetails) {
1315
+ const formattedQuery = questionMarksToOrderedParams(query);
1316
+ this.logSqlStatement(formattedQuery, options, requesterDetails);
1317
+ try {
1318
+ const response = await this.pool.query(formattedQuery, options);
1319
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1320
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1321
+ return response.rows[0];
1322
+ } catch (error) {
1323
+ console.error(error, query, options);
1324
+ if (RsError.isRsError(error)) throw error;
1325
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1326
+ throw new RsError("DUPLICATE", error.message);
1327
+ }
1328
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1329
+ }
1330
+ }
1331
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1332
+ async runQuery(query, options, requesterDetails) {
1333
+ const formattedQuery = questionMarksToOrderedParams(query);
1334
+ this.logSqlStatement(formattedQuery, options, requesterDetails);
1335
+ const queryUpdated = query.replace(/[\t\n]/g, " ");
1336
+ console.log(queryUpdated, options);
1337
+ try {
1338
+ const response = await this.pool.query(formattedQuery, options);
1339
+ return response.rows;
1340
+ } catch (error) {
1341
+ console.error(error, query, options);
1342
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1343
+ throw new RsError("DUPLICATE", error.message);
1344
+ }
1345
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1346
+ }
1347
+ }
1348
+ logSqlStatement(query, options, requesterDetails, prefix = "") {
1349
+ if (logger.level !== "silly") return;
1350
+ let sqlStatement = "";
1351
+ if (options.length === 0) {
1352
+ sqlStatement = query;
1353
+ } else {
1354
+ let stringIndex = 0;
1355
+ sqlStatement = query.replace(/\$\d+/g, () => {
1356
+ const value = options[stringIndex++];
1357
+ if (typeof value === "number") return value.toString();
1358
+ return import_pg_format2.default.literal(value);
1359
+ });
1360
+ }
1361
+ let initiator = "Anonymous";
1362
+ if ("userId" in requesterDetails && requesterDetails.userId)
1363
+ initiator = `User Id (${requesterDetails.userId.toString()})`;
1364
+ if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1365
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1366
+ ${sqlStatement}`);
1367
+ }
1368
+ };
1369
+
1292
1370
  // src/restura/sql/SqlEngine.ts
1293
1371
  var import_core_utils4 = require("@redskytech/core-utils");
1294
1372
  var SqlEngine = class {
@@ -1393,6 +1471,57 @@ var SqlEngine = class {
1393
1471
  // src/restura/sql/filterPsqlParser.ts
1394
1472
  var import_pegjs = __toESM(require("pegjs"));
1395
1473
  var filterSqlGrammar = `
1474
+ {
1475
+ // ported from pg-format but intentionally will add double quotes to every column
1476
+ function quoteSqlIdentity(value) {
1477
+ if (value === undefined || value === null) {
1478
+ throw new Error('SQL identifier cannot be null or undefined');
1479
+ } else if (value === false) {
1480
+ return '"f"';
1481
+ } else if (value === true) {
1482
+ return '"t"';
1483
+ } else if (value instanceof Date) {
1484
+ // return '"' + formatDate(value.toISOString()) + '"';
1485
+ } else if (value instanceof Buffer) {
1486
+ throw new Error('SQL identifier cannot be a buffer');
1487
+ } else if (Array.isArray(value) === true) {
1488
+ var temp = [];
1489
+ for (var i = 0; i < value.length; i++) {
1490
+ if (Array.isArray(value[i]) === true) {
1491
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1492
+ } else {
1493
+ // temp.push(quoteIdent(value[i]));
1494
+ }
1495
+ }
1496
+ return temp.toString();
1497
+ } else if (value === Object(value)) {
1498
+ throw new Error('SQL identifier cannot be an object');
1499
+ }
1500
+
1501
+ var ident = value.toString().slice(0); // create copy
1502
+
1503
+ // do not quote a valid, unquoted identifier
1504
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1505
+ // return ident;
1506
+ // }
1507
+
1508
+ var quoted = '"';
1509
+
1510
+ for (var i = 0; i < ident.length; i++) {
1511
+ var c = ident[i];
1512
+ if (c === '"') {
1513
+ quoted += c + c;
1514
+ } else {
1515
+ quoted += c;
1516
+ }
1517
+ }
1518
+
1519
+ quoted += '"';
1520
+
1521
+ return quoted;
1522
+ };
1523
+ }
1524
+
1396
1525
  start = expressionList
1397
1526
 
1398
1527
  expressionList =
@@ -1411,9 +1540,9 @@ negate = "!"
1411
1540
  operator = "and"i / "or"i
1412
1541
 
1413
1542
 
1414
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1543
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1415
1544
  /
1416
- text:text { return format.ident(text); }
1545
+ text:text { return quoteSqlIdentity(text); }
1417
1546
 
1418
1547
 
1419
1548
  text = text:[a-z0-9-_:@]i+ { return text.join("");}
@@ -1439,18 +1568,155 @@ var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
1439
1568
  var filterPsqlParser_default = filterPsqlParser;
1440
1569
 
1441
1570
  // src/restura/sql/PsqlEngine.ts
1571
+ var import_pg_diff_sync = __toESM(require("@wmfs/pg-diff-sync"));
1572
+ var import_pg_info = __toESM(require("@wmfs/pg-info"));
1573
+ var import_pg2 = require("pg");
1574
+ var systemUser = {
1575
+ role: "",
1576
+ host: "",
1577
+ ipAddress: "",
1578
+ isSystemUser: true
1579
+ };
1442
1580
  var PsqlEngine = class extends SqlEngine {
1443
1581
  constructor(psqlConnectionPool) {
1444
1582
  super();
1445
1583
  this.psqlConnectionPool = psqlConnectionPool;
1446
1584
  }
1447
- async diffDatabaseToSchema(schema) {
1448
- console.log(schema);
1449
- return Promise.resolve("");
1585
+ async createDatabaseFromSchema(schema, connection) {
1586
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1587
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1588
+ return sqlFullStatement;
1450
1589
  }
1451
1590
  generateDatabaseSchemaFromSchema(schema) {
1452
- console.log(schema);
1453
- return "";
1591
+ const sqlStatements = [];
1592
+ const enums = [];
1593
+ const indexes = [];
1594
+ for (const table of schema.database) {
1595
+ let sql = `CREATE TABLE "${table.name}"
1596
+ ( `;
1597
+ const tableColumns = [];
1598
+ for (const column of table.columns) {
1599
+ let columnSql = "";
1600
+ if (column.type === "ENUM") {
1601
+ enums.push(`CREATE TYPE ${schemaToPsqlType(column, table.name)} AS ENUM (${column.value});`);
1602
+ }
1603
+ columnSql += ` "${column.name}" ${schemaToPsqlType(column, table.name)}`;
1604
+ let value = column.value;
1605
+ if (column.type === "JSON") value = "";
1606
+ if (column.type === "JSONB") value = "";
1607
+ if (column.type === "DECIMAL" && value) {
1608
+ value = value.replace("-", ",").replace(/['"]/g, "");
1609
+ }
1610
+ if (value && column.type !== "ENUM") {
1611
+ columnSql += `(${value})`;
1612
+ } else if (column.length) columnSql += `(${column.length})`;
1613
+ if (column.isPrimary) {
1614
+ columnSql += " PRIMARY KEY ";
1615
+ }
1616
+ if (column.isUnique) {
1617
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
1618
+ }
1619
+ if (column.isNullable) columnSql += " NULL";
1620
+ else columnSql += " NOT NULL";
1621
+ if (column.default) columnSql += ` DEFAULT '${column.default}'`;
1622
+ tableColumns.push(columnSql);
1623
+ }
1624
+ sql += tableColumns.join(", \n");
1625
+ for (const index of table.indexes) {
1626
+ if (!index.isPrimaryKey) {
1627
+ let unique = " ";
1628
+ if (index.isUnique) unique = "UNIQUE ";
1629
+ indexes.push(
1630
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
1631
+ return `"${item}" ${index.order}`;
1632
+ }).join(", ")})`
1633
+ );
1634
+ }
1635
+ }
1636
+ sql += "\n);";
1637
+ sqlStatements.push(sql);
1638
+ }
1639
+ for (const table of schema.database) {
1640
+ if (!table.foreignKeys.length) continue;
1641
+ const sql = `ALTER TABLE "${table.name}" `;
1642
+ const constraints = [];
1643
+ for (const foreignKey of table.foreignKeys) {
1644
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
1645
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
1646
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
1647
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
1648
+ constraints.push(constraint);
1649
+ }
1650
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
1651
+ }
1652
+ for (const table of schema.database) {
1653
+ if (!table.checkConstraints.length) continue;
1654
+ const sql = `ALTER TABLE "${table.name}" `;
1655
+ const constraints = [];
1656
+ for (const check of table.checkConstraints) {
1657
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
1658
+ constraints.push(constraint);
1659
+ }
1660
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
1661
+ }
1662
+ sqlStatements.push(indexes.join(";\n"));
1663
+ return enums.join("\n") + "\n" + sqlStatements.join("\n\n");
1664
+ }
1665
+ async getScratchPool() {
1666
+ await this.psqlConnectionPool.runQuery(
1667
+ `DROP DATABASE IF EXISTS ${this.psqlConnectionPool.poolConfig.database}_scratch`,
1668
+ [],
1669
+ systemUser
1670
+ );
1671
+ await this.psqlConnectionPool.runQuery(
1672
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
1673
+ [],
1674
+ systemUser
1675
+ );
1676
+ const scratchPool = new PsqlPool({
1677
+ host: this.psqlConnectionPool.poolConfig.host,
1678
+ port: this.psqlConnectionPool.poolConfig.port,
1679
+ user: this.psqlConnectionPool.poolConfig.user,
1680
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
1681
+ password: this.psqlConnectionPool.poolConfig.password,
1682
+ max: this.psqlConnectionPool.poolConfig.max,
1683
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
1684
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1685
+ });
1686
+ return scratchPool;
1687
+ }
1688
+ async diffDatabaseToSchema(schema) {
1689
+ const scratchPool = await this.getScratchPool();
1690
+ await this.createDatabaseFromSchema(schema, scratchPool);
1691
+ const originalClient = new import_pg2.Client({
1692
+ database: this.psqlConnectionPool.poolConfig.database,
1693
+ user: this.psqlConnectionPool.poolConfig.user,
1694
+ password: this.psqlConnectionPool.poolConfig.password,
1695
+ host: this.psqlConnectionPool.poolConfig.host,
1696
+ port: this.psqlConnectionPool.poolConfig.port
1697
+ });
1698
+ const scratchClient = new import_pg2.Client({
1699
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
1700
+ user: this.psqlConnectionPool.poolConfig.user,
1701
+ password: this.psqlConnectionPool.poolConfig.password,
1702
+ host: this.psqlConnectionPool.poolConfig.host,
1703
+ port: this.psqlConnectionPool.poolConfig.port
1704
+ });
1705
+ await originalClient.connect();
1706
+ await scratchClient.connect();
1707
+ const info1 = await (0, import_pg_info.default)({
1708
+ client: originalClient,
1709
+ schema: "public"
1710
+ });
1711
+ const info2 = await (0, import_pg_info.default)({
1712
+ client: scratchClient,
1713
+ schema: "public"
1714
+ });
1715
+ const diff = (0, import_pg_diff_sync.default)(info1, info2);
1716
+ console.log("Schema differences:", diff);
1717
+ await originalClient.end();
1718
+ await scratchClient.end();
1719
+ return diff.join("\n");
1454
1720
  }
1455
1721
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1456
1722
  if (!item.subquery) return "";
@@ -1726,81 +1992,16 @@ ${sqlStatement};`,
1726
1992
  return whereClause;
1727
1993
  }
1728
1994
  };
1729
-
1730
- // src/restura/sql/PsqlPool.ts
1731
- var import_pg = __toESM(require("pg"));
1732
- var import_pg_format2 = __toESM(require("pg-format"));
1733
- var { Pool } = import_pg.default;
1734
- var PsqlPool = class {
1735
- constructor(poolConfig) {
1736
- this.poolConfig = poolConfig;
1737
- this.pool = new Pool(poolConfig);
1738
- this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1739
- logger.info("Connected to PostgreSQL database");
1740
- }).catch((error) => {
1741
- logger.error("Error connecting to database", error);
1742
- process.exit(1);
1743
- });
1744
- }
1745
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1746
- async queryOne(query, options, requesterDetails) {
1747
- const formattedQuery = questionMarksToOrderedParams(query);
1748
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1749
- try {
1750
- const response = await this.pool.query(formattedQuery, options);
1751
- if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1752
- else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1753
- return response.rows[0];
1754
- } catch (error) {
1755
- console.error(error, query, options);
1756
- if (RsError.isRsError(error)) throw error;
1757
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1758
- throw new RsError("DUPLICATE", error.message);
1759
- }
1760
- throw new RsError("DATABASE_ERROR", `${error.message}`);
1761
- }
1762
- }
1763
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1764
- async runQuery(query, options, requesterDetails) {
1765
- const formattedQuery = questionMarksToOrderedParams(query);
1766
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1767
- const queryUpdated = query.replace(/[\t\n]/g, " ");
1768
- console.log(queryUpdated, options);
1769
- try {
1770
- const response = await this.pool.query(formattedQuery, options);
1771
- return response.rows;
1772
- } catch (error) {
1773
- console.error(error, query, options);
1774
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1775
- throw new RsError("DUPLICATE", error.message);
1776
- }
1777
- throw new RsError("DATABASE_ERROR", `${error.message}`);
1778
- }
1779
- }
1780
- logSqlStatement(query, options, requesterDetails, prefix = "") {
1781
- if (logger.level !== "silly") return;
1782
- let sqlStatement = "";
1783
- if (options.length === 0) {
1784
- sqlStatement = query;
1785
- } else {
1786
- let stringIndex = 0;
1787
- sqlStatement = query.replace(/\$\d+/g, () => {
1788
- const value = options[stringIndex++];
1789
- if (typeof value === "number") return value.toString();
1790
- return import_pg_format2.default.literal(value);
1791
- });
1792
- }
1793
- let initiator = "Anonymous";
1794
- if ("userId" in requesterDetails && requesterDetails.userId)
1795
- initiator = `User Id (${requesterDetails.userId.toString()})`;
1796
- if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1797
- logger.silly(`${prefix}query by ${initiator}, Query ->
1798
- ${sqlStatement}`);
1799
- }
1995
+ var schemaToPsqlType = (column, tableName) => {
1996
+ if (column.hasAutoIncrement) return "BIGSERIAL";
1997
+ if (column.type === "ENUM") return `"${tableName}_${column.name}_enum"`;
1998
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
1999
+ if (column.type === "MEDIUMINT") return "INT";
2000
+ return column.type;
1800
2001
  };
1801
2002
 
1802
2003
  // src/restura/restura.ts
1803
- var { types } = import_pg2.default;
2004
+ var { types } = import_pg3.default;
1804
2005
  var ResturaEngine = class {
1805
2006
  constructor() {
1806
2007
  this.publicEndpoints = {