@restura/core 0.1.0-alpha.15 → 0.1.0-alpha.17

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
@@ -216,7 +216,7 @@ import { createHash } from "crypto";
216
216
  import * as express from "express";
217
217
  import fs3 from "fs";
218
218
  import path3 from "path";
219
- import pg4 from "pg";
219
+ import pg3 from "pg";
220
220
  import * as prettier3 from "prettier";
221
221
 
222
222
  // src/restura/sql/SqlUtils.ts
@@ -1235,7 +1235,8 @@ function insertObjectQuery(table, obj) {
1235
1235
  const params = Object.values(obj);
1236
1236
  const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1237
1237
  const values = params.map((value) => SQL`${value}`).join(", ");
1238
- const query = `INSERT INTO "${table}" (${columns})
1238
+ const query = `
1239
+ INSERT INTO "${table}" (${columns})
1239
1240
  VALUES (${values})
1240
1241
  RETURNING *`;
1241
1242
  return query;
@@ -1245,7 +1246,8 @@ function updateObjectQuery(table, obj, whereStatement) {
1245
1246
  for (const i in obj) {
1246
1247
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1247
1248
  }
1248
- return `UPDATE ${escapeColumnName(table)}
1249
+ return `
1250
+ UPDATE ${escapeColumnName(table)}
1249
1251
  SET ${setArray.join(", ")} ${whereStatement}
1250
1252
  RETURNING *`;
1251
1253
  }
@@ -1275,8 +1277,10 @@ var PsqlConnection = class {
1275
1277
  async queryOne(query, options, requesterDetails) {
1276
1278
  const formattedQuery = questionMarksToOrderedParams(query);
1277
1279
  this.logSqlStatement(formattedQuery, options, requesterDetails);
1280
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(requesterDetails)})
1281
+ `;
1278
1282
  try {
1279
- const response = await this.query(formattedQuery, options);
1283
+ const response = await this.query(queryMetadata + formattedQuery, options);
1280
1284
  if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1281
1285
  else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1282
1286
  return response.rows[0];
@@ -1293,8 +1297,10 @@ var PsqlConnection = class {
1293
1297
  async runQuery(query, options, requesterDetails) {
1294
1298
  const formattedQuery = questionMarksToOrderedParams(query);
1295
1299
  this.logSqlStatement(formattedQuery, options, requesterDetails);
1300
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(requesterDetails)})
1301
+ `;
1296
1302
  try {
1297
- const response = await this.query(formattedQuery, options);
1303
+ const response = await this.query(queryMetadata + formattedQuery, options);
1298
1304
  return response.rows;
1299
1305
  } catch (error) {
1300
1306
  console.error(error, query, options);
@@ -1545,6 +1551,110 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
1545
1551
  });
1546
1552
  var filterPsqlParser_default = filterPsqlParser;
1547
1553
 
1554
+ // src/restura/eventManager.ts
1555
+ import Bluebird from "bluebird";
1556
+ var EventManager = class {
1557
+ constructor() {
1558
+ this.actionHandlers = {
1559
+ DATABASE_ROW_DELETE: [],
1560
+ DATABASE_ROW_INSERT: [],
1561
+ DATABASE_COLUMN_UPDATE: []
1562
+ };
1563
+ }
1564
+ addRowInsertHandler(onInsert, filter) {
1565
+ this.actionHandlers.DATABASE_ROW_INSERT.push({
1566
+ callback: onInsert,
1567
+ filter
1568
+ });
1569
+ }
1570
+ addColumnChangeHandler(onUpdate, filter) {
1571
+ this.actionHandlers.DATABASE_COLUMN_UPDATE.push({
1572
+ callback: onUpdate,
1573
+ filter
1574
+ });
1575
+ }
1576
+ addRowDeleteHandler(onDelete, filter) {
1577
+ this.actionHandlers.DATABASE_ROW_DELETE.push({
1578
+ callback: onDelete,
1579
+ filter
1580
+ });
1581
+ }
1582
+ async fireActionFromDbTrigger(sqlMutationData, result) {
1583
+ if (sqlMutationData.mutationType === "INSERT") {
1584
+ await this.fireInsertActions(sqlMutationData, result);
1585
+ } else if (sqlMutationData.mutationType === "UPDATE") {
1586
+ await this.fireUpdateActions(sqlMutationData, result);
1587
+ } else if (sqlMutationData.mutationType === "DELETE") {
1588
+ await this.fireDeleteActions(sqlMutationData, result);
1589
+ }
1590
+ }
1591
+ async fireInsertActions(data, triggerResult) {
1592
+ await Bluebird.map(
1593
+ this.actionHandlers.DATABASE_ROW_INSERT,
1594
+ ({ callback, filter }) => {
1595
+ if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
1596
+ const insertData = {
1597
+ tableName: triggerResult.table,
1598
+ insertId: triggerResult.record.id,
1599
+ insertObject: triggerResult.record,
1600
+ requesterDetails: data.requesterDetails
1601
+ };
1602
+ callback(insertData, data.requesterDetails);
1603
+ },
1604
+ { concurrency: 10 }
1605
+ );
1606
+ }
1607
+ async fireDeleteActions(data, triggerResult) {
1608
+ await Bluebird.map(
1609
+ this.actionHandlers.DATABASE_ROW_DELETE,
1610
+ ({ callback, filter }) => {
1611
+ if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
1612
+ const deleteData = {
1613
+ tableName: triggerResult.table,
1614
+ deletedRow: triggerResult.previousRecord,
1615
+ requesterDetails: data.requesterDetails
1616
+ };
1617
+ callback(deleteData, data.requesterDetails);
1618
+ },
1619
+ { concurrency: 10 }
1620
+ );
1621
+ }
1622
+ async fireUpdateActions(data, triggerResult) {
1623
+ await Bluebird.map(
1624
+ this.actionHandlers.DATABASE_COLUMN_UPDATE,
1625
+ ({ callback, filter }) => {
1626
+ if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
1627
+ const columnChangeData = {
1628
+ tableName: triggerResult.table,
1629
+ rowId: triggerResult.record.id,
1630
+ newData: triggerResult.record,
1631
+ oldData: triggerResult.previousRecord,
1632
+ requesterDetails: data.requesterDetails
1633
+ };
1634
+ callback(columnChangeData, data.requesterDetails);
1635
+ },
1636
+ { concurrency: 10 }
1637
+ );
1638
+ }
1639
+ hasHandlersForEventType(eventType, filter, triggerResult) {
1640
+ if (filter) {
1641
+ switch (eventType) {
1642
+ case "DATABASE_ROW_INSERT":
1643
+ case "DATABASE_ROW_DELETE":
1644
+ if (filter.tableName && filter.tableName !== triggerResult.table) return false;
1645
+ break;
1646
+ case "DATABASE_COLUMN_UPDATE":
1647
+ const filterColumnChange = filter;
1648
+ if (filterColumnChange.tableName !== filter.tableName) return false;
1649
+ break;
1650
+ }
1651
+ }
1652
+ return true;
1653
+ }
1654
+ };
1655
+ var eventManager = new EventManager();
1656
+ var eventManager_default = eventManager;
1657
+
1548
1658
  // src/restura/sql/PsqlEngine.ts
1549
1659
  var { Client } = pg2;
1550
1660
  var systemUser = {
@@ -1554,9 +1664,49 @@ var systemUser = {
1554
1664
  isSystemUser: true
1555
1665
  };
1556
1666
  var PsqlEngine = class extends SqlEngine {
1557
- constructor(psqlConnectionPool) {
1667
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1558
1668
  super();
1559
1669
  this.psqlConnectionPool = psqlConnectionPool;
1670
+ if (shouldListenForDbTriggers) {
1671
+ this.setupTriggerListeners = this.listenForDbTriggers();
1672
+ }
1673
+ }
1674
+ async close() {
1675
+ if (this.triggerClient) {
1676
+ await this.triggerClient.end();
1677
+ }
1678
+ }
1679
+ async listenForDbTriggers() {
1680
+ this.triggerClient = new Client({
1681
+ user: this.psqlConnectionPool.poolConfig.user,
1682
+ host: this.psqlConnectionPool.poolConfig.host,
1683
+ database: this.psqlConnectionPool.poolConfig.database,
1684
+ password: this.psqlConnectionPool.poolConfig.password,
1685
+ port: this.psqlConnectionPool.poolConfig.port,
1686
+ connectionTimeoutMillis: 2e3
1687
+ });
1688
+ await this.triggerClient.connect();
1689
+ const promises = [];
1690
+ promises.push(this.triggerClient.query("LISTEN insert"));
1691
+ promises.push(this.triggerClient.query("LISTEN update"));
1692
+ promises.push(this.triggerClient.query("LISTEN delete"));
1693
+ await Promise.all(promises);
1694
+ this.triggerClient.on("notification", async (msg) => {
1695
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1696
+ const payload = JSON.parse(msg.payload);
1697
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1698
+ }
1699
+ });
1700
+ }
1701
+ async handleTrigger(payload, mutationType) {
1702
+ const findRequesterDetailsRegex = /^--QUERY_METADATA\(\{.*\}\)/;
1703
+ let requesterDetails = {};
1704
+ const match = payload.query.match(findRequesterDetailsRegex);
1705
+ if (match) {
1706
+ const jsonString = match[0].slice(match[0].indexOf("{"), match[0].lastIndexOf("}") + 1);
1707
+ requesterDetails = ObjectUtils4.safeParse(jsonString);
1708
+ await eventManager_default.fireActionFromDbTrigger({ requesterDetails, mutationType }, payload);
1709
+ }
1560
1710
  }
1561
1711
  async createDatabaseFromSchema(schema, connection) {
1562
1712
  const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
@@ -1567,7 +1717,11 @@ var PsqlEngine = class extends SqlEngine {
1567
1717
  const sqlStatements = [];
1568
1718
  const enums = [];
1569
1719
  const indexes = [];
1720
+ const triggers = [];
1570
1721
  for (const table of schema.database) {
1722
+ triggers.push(this.createInsertTriggers(table.name));
1723
+ triggers.push(this.createUpdateTrigger(table.name));
1724
+ triggers.push(this.createDeleteTrigger(table.name));
1571
1725
  let sql = `CREATE TABLE "${table.name}"
1572
1726
  ( `;
1573
1727
  const tableColumns = [];
@@ -1605,7 +1759,7 @@ var PsqlEngine = class extends SqlEngine {
1605
1759
  indexes.push(
1606
1760
  ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
1607
1761
  return `"${item}" ${index.order}`;
1608
- }).join(", ")})`
1762
+ }).join(", ")});`
1609
1763
  );
1610
1764
  }
1611
1765
  }
@@ -1635,7 +1789,8 @@ var PsqlEngine = class extends SqlEngine {
1635
1789
  }
1636
1790
  sqlStatements.push(sql + constraints.join(",\n") + ";");
1637
1791
  }
1638
- sqlStatements.push(indexes.join(";\n"));
1792
+ sqlStatements.push(indexes.join("\n"));
1793
+ sqlStatements.push(triggers.join("\n"));
1639
1794
  return enums.join("\n") + "\n" + sqlStatements.join("\n\n");
1640
1795
  }
1641
1796
  async getScratchPool() {
@@ -1703,8 +1858,7 @@ var PsqlEngine = class extends SqlEngine {
1703
1858
  )) {
1704
1859
  return "'[]'";
1705
1860
  }
1706
- return `COALESCE((
1707
- SELECT JSON_AGG(JSON_BUILD_OBJECT(
1861
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1708
1862
  ${item.subquery.properties.map((nestedItem) => {
1709
1863
  if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1710
1864
  ...routeData.joins,
@@ -1713,7 +1867,7 @@ var PsqlEngine = class extends SqlEngine {
1713
1867
  return;
1714
1868
  }
1715
1869
  if (nestedItem.subquery) {
1716
- return `"${nestedItem.name}", ${this.createNestedSelect(
1870
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1717
1871
  // recursion
1718
1872
  req,
1719
1873
  schema,
@@ -1724,7 +1878,7 @@ var PsqlEngine = class extends SqlEngine {
1724
1878
  )}`;
1725
1879
  }
1726
1880
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1727
- }).filter(Boolean).join(",")}
1881
+ }).filter(Boolean).join(", ")}
1728
1882
  ))
1729
1883
  FROM
1730
1884
  "${item.subquery.table}"
@@ -1852,10 +2006,12 @@ var PsqlEngine = class extends SqlEngine {
1852
2006
  req.requesterDetails.role,
1853
2007
  sqlParams
1854
2008
  );
1855
- let deleteStatement = `DELETE
1856
- FROM "${routeData.table}" ${joinStatement}`;
1857
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1858
- deleteStatement += ";";
2009
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2010
+ if (whereClause.replace(/\s/g, "") === "") {
2011
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2012
+ }
2013
+ const deleteStatement = `
2014
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
1859
2015
  await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1860
2016
  return true;
1861
2017
  }
@@ -1918,17 +2074,17 @@ var PsqlEngine = class extends SqlEngine {
1918
2074
  );
1919
2075
  let operator = item.operator;
1920
2076
  if (operator === "LIKE") {
1921
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2077
+ item.value = `%${item.value}%`;
1922
2078
  } else if (operator === "STARTS WITH") {
1923
2079
  operator = "LIKE";
1924
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2080
+ item.value = `${item.value}%`;
1925
2081
  } else if (operator === "ENDS WITH") {
1926
2082
  operator = "LIKE";
1927
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2083
+ item.value = `%${item.value}`;
1928
2084
  }
1929
2085
  const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
1930
2086
  const escapedValue = SQL`${replacedValue}`;
1931
- whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
2087
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
1932
2088
  `;
1933
2089
  });
1934
2090
  const data = req.data;
@@ -1962,7 +2118,67 @@ var PsqlEngine = class extends SqlEngine {
1962
2118
  }
1963
2119
  return whereClause;
1964
2120
  }
2121
+ createUpdateTrigger(tableName) {
2122
+ return `
2123
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2124
+ RETURNS TRIGGER AS $$
2125
+ BEGIN
2126
+ PERFORM pg_notify('update', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2127
+ RETURN NEW;
2128
+ END;
2129
+ $$ LANGUAGE plpgsql;
2130
+
2131
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2132
+ AFTER UPDATE ON "${tableName}"
2133
+ FOR EACH ROW
2134
+ EXECUTE FUNCTION notify_${tableName}_update();
2135
+ `;
2136
+ }
2137
+ createDeleteTrigger(tableName) {
2138
+ return `
2139
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2140
+ RETURNS TRIGGER AS $$
2141
+ BEGIN
2142
+ PERFORM pg_notify('delete', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2143
+ RETURN NEW;
2144
+ END;
2145
+ $$ LANGUAGE plpgsql;
2146
+
2147
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2148
+ AFTER DELETE ON "${tableName}"
2149
+ FOR EACH ROW
2150
+ EXECUTE FUNCTION notify_${tableName}_delete();
2151
+ `;
2152
+ }
2153
+ createInsertTriggers(tableName) {
2154
+ return `
2155
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2156
+ RETURNS TRIGGER AS $$
2157
+ BEGIN
2158
+ PERFORM pg_notify('insert', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2159
+ RETURN NEW;
2160
+ END;
2161
+ $$ LANGUAGE plpgsql;
2162
+
2163
+ CREATE TRIGGER "${tableName}_insert"
2164
+ AFTER INSERT ON "${tableName}"
2165
+ FOR EACH ROW
2166
+ EXECUTE FUNCTION notify_${tableName}_insert();
2167
+ `;
2168
+ }
1965
2169
  };
2170
+ __decorateClass([
2171
+ boundMethod
2172
+ ], PsqlEngine.prototype, "handleTrigger", 1);
2173
+ __decorateClass([
2174
+ boundMethod
2175
+ ], PsqlEngine.prototype, "createUpdateTrigger", 1);
2176
+ __decorateClass([
2177
+ boundMethod
2178
+ ], PsqlEngine.prototype, "createDeleteTrigger", 1);
2179
+ __decorateClass([
2180
+ boundMethod
2181
+ ], PsqlEngine.prototype, "createInsertTriggers", 1);
1966
2182
  function schemaToPsqlType(column, tableName) {
1967
2183
  if (column.hasAutoIncrement) return "BIGSERIAL";
1968
2184
  if (column.type === "ENUM") return `"${tableName}_${column.name}_enum"`;
@@ -1971,35 +2187,6 @@ function schemaToPsqlType(column, tableName) {
1971
2187
  return column.type;
1972
2188
  }
1973
2189
 
1974
- // src/restura/sql/PsqlTransaction.ts
1975
- import pg3 from "pg";
1976
- var { Client: Client2 } = pg3;
1977
- var PsqlTransaction = class extends PsqlConnection {
1978
- constructor(clientConfig) {
1979
- super();
1980
- this.clientConfig = clientConfig;
1981
- this.client = new Client2(clientConfig);
1982
- this.beginTransactionPromise = this.beginTransaction();
1983
- }
1984
- async beginTransaction() {
1985
- return this.query("BEGIN");
1986
- }
1987
- async rollback() {
1988
- return this.query("ROLLBACK");
1989
- }
1990
- async commit() {
1991
- return this.query("COMMIT");
1992
- }
1993
- async release() {
1994
- return this.client.end();
1995
- }
1996
- async query(query, values) {
1997
- await this.client.connect();
1998
- await this.beginTransactionPromise;
1999
- return this.client.query(query, values);
2000
- }
2001
- };
2002
-
2003
2190
  // src/restura/compareSchema.ts
2004
2191
  import cloneDeep from "lodash.clonedeep";
2005
2192
  var CompareSchema = class {
@@ -2091,7 +2278,7 @@ var compareSchema = new CompareSchema();
2091
2278
  var compareSchema_default = compareSchema;
2092
2279
 
2093
2280
  // src/restura/restura.ts
2094
- var { types } = pg4;
2281
+ var { types } = pg3;
2095
2282
  var ResturaEngine = class {
2096
2283
  constructor() {
2097
2284
  this.publicEndpoints = {
@@ -2466,6 +2653,35 @@ var setupPgReturnTypes = () => {
2466
2653
  };
2467
2654
  setupPgReturnTypes();
2468
2655
  var restura = new ResturaEngine();
2656
+
2657
+ // src/restura/sql/PsqlTransaction.ts
2658
+ import pg4 from "pg";
2659
+ var { Client: Client2 } = pg4;
2660
+ var PsqlTransaction = class extends PsqlConnection {
2661
+ constructor(clientConfig) {
2662
+ super();
2663
+ this.clientConfig = clientConfig;
2664
+ this.client = new Client2(clientConfig);
2665
+ this.beginTransactionPromise = this.beginTransaction();
2666
+ }
2667
+ async beginTransaction() {
2668
+ return this.query("BEGIN");
2669
+ }
2670
+ async rollback() {
2671
+ return this.query("ROLLBACK");
2672
+ }
2673
+ async commit() {
2674
+ return this.query("COMMIT");
2675
+ }
2676
+ async release() {
2677
+ return this.client.end();
2678
+ }
2679
+ async query(query, values) {
2680
+ await this.client.connect();
2681
+ await this.beginTransactionPromise;
2682
+ return this.client.query(query, values);
2683
+ }
2684
+ };
2469
2685
  export {
2470
2686
  HtmlStatusCodes,
2471
2687
  PsqlConnection,
@@ -2474,6 +2690,7 @@ export {
2474
2690
  RsError,
2475
2691
  SQL,
2476
2692
  escapeColumnName,
2693
+ eventManager_default as eventManager,
2477
2694
  insertObjectQuery,
2478
2695
  isValueNumber2 as isValueNumber,
2479
2696
  logger,