@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.mjs CHANGED
@@ -494,7 +494,7 @@ var ApiTree = class _ApiTree {
494
494
  break;
495
495
  }
496
496
  }
497
- return `'${p.name}'${p.required ? "" : "?"}:${requestType}`;
497
+ return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
498
498
  }).join(";\n")}${ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
499
499
  `;
500
500
  modelString += `}`;
@@ -518,18 +518,18 @@ var ApiTree = class _ApiTree {
518
518
  return nested;
519
519
  }
520
520
  getNameAndType(p) {
521
- let responseType = "any", optional = false, array = false;
521
+ let responseType = "any", isNullable = false, array = false;
522
522
  if (p.selector) {
523
- ({ responseType, optional } = this.getTypeFromTable(p.selector, p.name));
523
+ ({ responseType, isNullable } = this.getTypeFromTable(p.selector, p.name));
524
524
  } else if (p.subquery) {
525
525
  responseType = this.getFields(p.subquery.properties);
526
526
  array = true;
527
527
  }
528
- return `${p.name}${optional ? "?" : ""}:${responseType}${array ? "[]" : ""}`;
528
+ return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
529
529
  }
530
530
  getTypeFromTable(selector, name) {
531
531
  const path4 = selector.split(".");
532
- if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { responseType: "any", optional: false };
532
+ if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { responseType: "any", isNullable: false };
533
533
  let tableName = path4.length == 2 ? path4[0] : name;
534
534
  const columnName = path4.length == 2 ? path4[1] : path4[0];
535
535
  let table = this.database.find((t) => t.name == tableName);
@@ -539,10 +539,10 @@ var ApiTree = class _ApiTree {
539
539
  table = this.database.find((t) => t.name == tableName);
540
540
  }
541
541
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
542
- if (!table || !column) return { responseType: "any", optional: false };
542
+ if (!table || !column) return { responseType: "any", isNullable: false };
543
543
  return {
544
544
  responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
545
- optional: column.roles.length > 0 || column.isNullable
545
+ isNullable: column.roles.length > 0 || column.isNullable
546
546
  };
547
547
  }
548
548
  };
@@ -748,6 +748,7 @@ var joinDataSchema = z3.object({
748
748
  var requestDataSchema = z3.object({
749
749
  name: z3.string(),
750
750
  required: z3.boolean(),
751
+ isNullable: z3.boolean().optional().default(false),
751
752
  validator: z3.array(validatorDataSchema)
752
753
  }).strict();
753
754
  var responseDataSchema = z3.object({
@@ -1023,6 +1024,7 @@ function validateRequestParams(req, routeData, validationSchema) {
1023
1024
  });
1024
1025
  }
1025
1026
  function validateRequestSingleParam(requestValue, requestParam) {
1027
+ if (requestParam.isNullable && requestValue === null) return;
1026
1028
  requestParam.validator.forEach((validator) => {
1027
1029
  switch (validator.type) {
1028
1030
  case "TYPE_CHECK":
@@ -1202,6 +1204,10 @@ function convertTable(table) {
1202
1204
  // src/restura/sql/PsqlEngine.ts
1203
1205
  import { ObjectUtils as ObjectUtils4 } from "@redskytech/core-utils";
1204
1206
 
1207
+ // src/restura/sql/PsqlPool.ts
1208
+ import pg from "pg";
1209
+ import format3 from "pg-format";
1210
+
1205
1211
  // src/restura/sql/PsqlUtils.ts
1206
1212
  import format2 from "pg-format";
1207
1213
  function escapeColumnName(columnName) {
@@ -1237,7 +1243,9 @@ function isValueNumber2(value) {
1237
1243
  function SQL(strings, ...values) {
1238
1244
  let query = strings[0];
1239
1245
  values.forEach((value, index) => {
1240
- if (isValueNumber2(value)) {
1246
+ if (typeof value === "boolean") {
1247
+ query += value;
1248
+ } else if (typeof value === "number") {
1241
1249
  query += value;
1242
1250
  } else {
1243
1251
  query += format2.literal(value);
@@ -1247,6 +1255,76 @@ function SQL(strings, ...values) {
1247
1255
  return query;
1248
1256
  }
1249
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
+
1250
1328
  // src/restura/sql/SqlEngine.ts
1251
1329
  import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
1252
1330
  var SqlEngine = class {
@@ -1351,6 +1429,57 @@ var SqlEngine = class {
1351
1429
  // src/restura/sql/filterPsqlParser.ts
1352
1430
  import peg from "pegjs";
1353
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
+
1354
1483
  start = expressionList
1355
1484
 
1356
1485
  expressionList =
@@ -1369,9 +1498,9 @@ negate = "!"
1369
1498
  operator = "and"i / "or"i
1370
1499
 
1371
1500
 
1372
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1501
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1373
1502
  /
1374
- text:text { return format.ident(text); }
1503
+ text:text { return quoteSqlIdentity(text); }
1375
1504
 
1376
1505
 
1377
1506
  text = text:[a-z0-9-_:@]i+ { return text.join("");}
@@ -1397,18 +1526,155 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
1397
1526
  var filterPsqlParser_default = filterPsqlParser;
1398
1527
 
1399
1528
  // src/restura/sql/PsqlEngine.ts
1529
+ import getDiff from "@wmfs/pg-diff-sync";
1530
+ import pgInfo from "@wmfs/pg-info";
1531
+ import { Client } from "pg";
1532
+ var systemUser = {
1533
+ role: "",
1534
+ host: "",
1535
+ ipAddress: "",
1536
+ isSystemUser: true
1537
+ };
1400
1538
  var PsqlEngine = class extends SqlEngine {
1401
1539
  constructor(psqlConnectionPool) {
1402
1540
  super();
1403
1541
  this.psqlConnectionPool = psqlConnectionPool;
1404
1542
  }
1405
- async diffDatabaseToSchema(schema) {
1406
- console.log(schema);
1407
- return Promise.resolve("");
1543
+ async createDatabaseFromSchema(schema, connection) {
1544
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1545
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1546
+ return sqlFullStatement;
1408
1547
  }
1409
1548
  generateDatabaseSchemaFromSchema(schema) {
1410
- console.log(schema);
1411
- 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");
1412
1678
  }
1413
1679
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1414
1680
  if (!item.subquery) return "";
@@ -1684,77 +1950,12 @@ ${sqlStatement};`,
1684
1950
  return whereClause;
1685
1951
  }
1686
1952
  };
1687
-
1688
- // src/restura/sql/PsqlPool.ts
1689
- import pg from "pg";
1690
- import format3 from "pg-format";
1691
- var { Pool } = pg;
1692
- var PsqlPool = class {
1693
- constructor(poolConfig) {
1694
- this.poolConfig = poolConfig;
1695
- this.pool = new Pool(poolConfig);
1696
- this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1697
- logger.info("Connected to PostgreSQL database");
1698
- }).catch((error) => {
1699
- logger.error("Error connecting to database", error);
1700
- process.exit(1);
1701
- });
1702
- }
1703
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1704
- async queryOne(query, options, requesterDetails) {
1705
- const formattedQuery = questionMarksToOrderedParams(query);
1706
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1707
- try {
1708
- const response = await this.pool.query(formattedQuery, options);
1709
- if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1710
- else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1711
- return response.rows[0];
1712
- } catch (error) {
1713
- console.error(error, query, options);
1714
- if (RsError.isRsError(error)) throw error;
1715
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1716
- throw new RsError("DUPLICATE", error.message);
1717
- }
1718
- throw new RsError("DATABASE_ERROR", `${error.message}`);
1719
- }
1720
- }
1721
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1722
- async runQuery(query, options, requesterDetails) {
1723
- const formattedQuery = questionMarksToOrderedParams(query);
1724
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1725
- const queryUpdated = query.replace(/[\t\n]/g, " ");
1726
- console.log(queryUpdated, options);
1727
- try {
1728
- const response = await this.pool.query(formattedQuery, options);
1729
- return response.rows;
1730
- } catch (error) {
1731
- console.error(error, query, options);
1732
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1733
- throw new RsError("DUPLICATE", error.message);
1734
- }
1735
- throw new RsError("DATABASE_ERROR", `${error.message}`);
1736
- }
1737
- }
1738
- logSqlStatement(query, options, requesterDetails, prefix = "") {
1739
- if (logger.level !== "silly") return;
1740
- let sqlStatement = "";
1741
- if (options.length === 0) {
1742
- sqlStatement = query;
1743
- } else {
1744
- let stringIndex = 0;
1745
- sqlStatement = query.replace(/\$\d+/g, () => {
1746
- const value = options[stringIndex++];
1747
- if (typeof value === "number") return value.toString();
1748
- return format3.literal(value);
1749
- });
1750
- }
1751
- let initiator = "Anonymous";
1752
- if ("userId" in requesterDetails && requesterDetails.userId)
1753
- initiator = `User Id (${requesterDetails.userId.toString()})`;
1754
- if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1755
- logger.silly(`${prefix}query by ${initiator}, Query ->
1756
- ${sqlStatement}`);
1757
- }
1953
+ var schemaToPsqlType = (column, tableName) => {
1954
+ if (column.hasAutoIncrement) return "BIGSERIAL";
1955
+ if (column.type === "ENUM") return `"${tableName}_${column.name}_enum"`;
1956
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
1957
+ if (column.type === "MEDIUMINT") return "INT";
1958
+ return column.type;
1758
1959
  };
1759
1960
 
1760
1961
  // src/restura/restura.ts