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

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.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import winston from 'winston';
2
2
  import * as express from 'express';
3
3
  import { z } from 'zod';
4
- import { QueryResultRow, QueryConfigValues, QueryResult, PoolConfig, Pool, ClientConfig, Client } from 'pg';
4
+ import { QueryResultRow, QueryConfigValues, QueryResult, PoolConfig, Pool } from 'pg';
5
5
  import { IncomingHttpHeaders } from 'http2';
6
6
 
7
7
  declare const logger: winston.Logger;
@@ -1392,10 +1392,10 @@ interface AsyncExpressApplication {
1392
1392
  }
1393
1393
 
1394
1394
  declare abstract class PsqlConnection {
1395
- constructor();
1395
+ protected constructor();
1396
1396
  protected abstract query<R extends QueryResultRow = QueryResultRow, T extends Array<unknown> = unknown[]>(query: string, values?: QueryConfigValues<T>): Promise<QueryResult<R>>;
1397
- queryOne(query: string, options: any[], requesterDetails: RequesterDetails): Promise<QueryResultRow>;
1398
- runQuery(query: string, options: any[], requesterDetails: RequesterDetails): Promise<QueryResultRow[]>;
1397
+ queryOne<T>(query: string, options: any[], requesterDetails: RequesterDetails): Promise<T>;
1398
+ runQuery<T>(query: string, options: any[], requesterDetails: RequesterDetails): Promise<T[]>;
1399
1399
  private logSqlStatement;
1400
1400
  }
1401
1401
 
@@ -1406,18 +1406,6 @@ declare class PsqlPool extends PsqlConnection {
1406
1406
  protected query<R extends QueryResultRow = QueryResultRow, T extends Array<unknown> = unknown[]>(query: string, values?: QueryConfigValues<T>): Promise<QueryResult<R>>;
1407
1407
  }
1408
1408
 
1409
- declare class PsqlTransaction extends PsqlConnection {
1410
- clientConfig: ClientConfig;
1411
- client: Client;
1412
- private beginTransactionPromise;
1413
- constructor(clientConfig: ClientConfig);
1414
- private beginTransaction;
1415
- rollback(): Promise<QueryResult<QueryResultRow>>;
1416
- commit(): Promise<QueryResult<QueryResultRow>>;
1417
- release(): Promise<void>;
1418
- protected query<R extends QueryResultRow = QueryResultRow, T extends Array<unknown> = unknown[]>(query: string, values?: QueryConfigValues<T>): Promise<QueryResult<R>>;
1419
- }
1420
-
1421
1409
  declare class ResturaEngine {
1422
1410
  resturaConfig: ResturaConfigSchema;
1423
1411
  private resturaRouter;
@@ -1516,4 +1504,45 @@ declare function updateObjectQuery(table: string, obj: DynamicObject, whereState
1516
1504
  declare function isValueNumber(value: unknown): value is number;
1517
1505
  declare function SQL(strings: any, ...values: any): any;
1518
1506
 
1519
- export { type ApiMethod, type AsyncExpressApplication, type AuthenticateHandler, type AuthenticationUserDetails, type ConjunctionTypes, type DynamicObject, type ErrorCode, HtmlStatusCodes, type LoginDetails, type MatchTypes, type PageQuery, PsqlConnection, PsqlPool, PsqlTransaction, type RequesterDetails, type ResponseType, type ResponseTypeMap, RsError, type RsErrorData, type RsErrorInternalData, type RsHeaders, type RsPagedResponseData, type RsRequest, type RsResponse, type RsResponseData, type RsRouteHandler, SQL, type SchemaChangeValue, type SchemaPreview, type StandardOrderTypes, type ValidAuthenticationCallback, type ValidatorString, escapeColumnName, insertObjectQuery, isValueNumber, logger, questionMarksToOrderedParams, restura, updateObjectQuery };
1507
+ type EventType = 'DATABASE_ROW_DELETE' | 'DATABASE_ROW_INSERT' | 'DATABASE_COLUMN_UPDATE' | 'WEBHOOK';
1508
+ type MutationType = 'INSERT' | 'UPDATE' | 'DELETE';
1509
+ interface SqlMutationData {
1510
+ mutationType: MutationType;
1511
+ requesterDetails: RequesterDetails;
1512
+ }
1513
+ interface DatabaseActionData {
1514
+ tableName: string;
1515
+ requesterDetails: RequesterDetails;
1516
+ }
1517
+ interface ActionRowInsertData<T = DynamicObject> extends DatabaseActionData {
1518
+ insertId: number;
1519
+ insertObject: T;
1520
+ }
1521
+ interface ActionRowDeleteData extends DatabaseActionData {
1522
+ deletedRow: DynamicObject;
1523
+ }
1524
+ interface ActionColumnChangeData extends DatabaseActionData {
1525
+ tableName: string;
1526
+ rowId: number;
1527
+ newData: DynamicObject;
1528
+ oldData: DynamicObject;
1529
+ }
1530
+ interface ActionRowInsertFilter {
1531
+ tableName: string;
1532
+ }
1533
+ interface ActionRowDeleteFilter {
1534
+ tableName: string;
1535
+ }
1536
+ interface ActionColumnChangeFilter {
1537
+ tableName: string;
1538
+ }
1539
+ type TriggerResult = {
1540
+ table: string;
1541
+ insertId?: number;
1542
+ query: string;
1543
+ record: DynamicObject;
1544
+ previousRecord: DynamicObject;
1545
+ requesterId: number;
1546
+ };
1547
+
1548
+ export { type ActionColumnChangeData, type ActionColumnChangeFilter, type ActionRowDeleteData, type ActionRowDeleteFilter, type ActionRowInsertData, type ActionRowInsertFilter, type ApiMethod, type AsyncExpressApplication, type AuthenticateHandler, type AuthenticationUserDetails, type ConjunctionTypes, type DatabaseActionData, type DynamicObject, type ErrorCode, type EventType, HtmlStatusCodes, type LoginDetails, type MatchTypes, type MutationType, type PageQuery, PsqlPool, type RequesterDetails, type ResponseType, type ResponseTypeMap, RsError, type RsErrorData, type RsErrorInternalData, type RsHeaders, type RsPagedResponseData, type RsRequest, type RsResponse, type RsResponseData, type RsRouteHandler, SQL, type SchemaChangeValue, type SchemaPreview, type SqlMutationData, type StandardOrderTypes, type TriggerResult, type ValidAuthenticationCallback, type ValidatorString, escapeColumnName, insertObjectQuery, isValueNumber, logger, questionMarksToOrderedParams, restura, updateObjectQuery };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import winston from 'winston';
2
2
  import * as express from 'express';
3
3
  import { z } from 'zod';
4
- import { QueryResultRow, QueryConfigValues, QueryResult, PoolConfig, Pool, ClientConfig, Client } from 'pg';
4
+ import { QueryResultRow, QueryConfigValues, QueryResult, PoolConfig, Pool } from 'pg';
5
5
  import { IncomingHttpHeaders } from 'http2';
6
6
 
7
7
  declare const logger: winston.Logger;
@@ -1392,10 +1392,10 @@ interface AsyncExpressApplication {
1392
1392
  }
1393
1393
 
1394
1394
  declare abstract class PsqlConnection {
1395
- constructor();
1395
+ protected constructor();
1396
1396
  protected abstract query<R extends QueryResultRow = QueryResultRow, T extends Array<unknown> = unknown[]>(query: string, values?: QueryConfigValues<T>): Promise<QueryResult<R>>;
1397
- queryOne(query: string, options: any[], requesterDetails: RequesterDetails): Promise<QueryResultRow>;
1398
- runQuery(query: string, options: any[], requesterDetails: RequesterDetails): Promise<QueryResultRow[]>;
1397
+ queryOne<T>(query: string, options: any[], requesterDetails: RequesterDetails): Promise<T>;
1398
+ runQuery<T>(query: string, options: any[], requesterDetails: RequesterDetails): Promise<T[]>;
1399
1399
  private logSqlStatement;
1400
1400
  }
1401
1401
 
@@ -1406,18 +1406,6 @@ declare class PsqlPool extends PsqlConnection {
1406
1406
  protected query<R extends QueryResultRow = QueryResultRow, T extends Array<unknown> = unknown[]>(query: string, values?: QueryConfigValues<T>): Promise<QueryResult<R>>;
1407
1407
  }
1408
1408
 
1409
- declare class PsqlTransaction extends PsqlConnection {
1410
- clientConfig: ClientConfig;
1411
- client: Client;
1412
- private beginTransactionPromise;
1413
- constructor(clientConfig: ClientConfig);
1414
- private beginTransaction;
1415
- rollback(): Promise<QueryResult<QueryResultRow>>;
1416
- commit(): Promise<QueryResult<QueryResultRow>>;
1417
- release(): Promise<void>;
1418
- protected query<R extends QueryResultRow = QueryResultRow, T extends Array<unknown> = unknown[]>(query: string, values?: QueryConfigValues<T>): Promise<QueryResult<R>>;
1419
- }
1420
-
1421
1409
  declare class ResturaEngine {
1422
1410
  resturaConfig: ResturaConfigSchema;
1423
1411
  private resturaRouter;
@@ -1516,4 +1504,45 @@ declare function updateObjectQuery(table: string, obj: DynamicObject, whereState
1516
1504
  declare function isValueNumber(value: unknown): value is number;
1517
1505
  declare function SQL(strings: any, ...values: any): any;
1518
1506
 
1519
- export { type ApiMethod, type AsyncExpressApplication, type AuthenticateHandler, type AuthenticationUserDetails, type ConjunctionTypes, type DynamicObject, type ErrorCode, HtmlStatusCodes, type LoginDetails, type MatchTypes, type PageQuery, PsqlConnection, PsqlPool, PsqlTransaction, type RequesterDetails, type ResponseType, type ResponseTypeMap, RsError, type RsErrorData, type RsErrorInternalData, type RsHeaders, type RsPagedResponseData, type RsRequest, type RsResponse, type RsResponseData, type RsRouteHandler, SQL, type SchemaChangeValue, type SchemaPreview, type StandardOrderTypes, type ValidAuthenticationCallback, type ValidatorString, escapeColumnName, insertObjectQuery, isValueNumber, logger, questionMarksToOrderedParams, restura, updateObjectQuery };
1507
+ type EventType = 'DATABASE_ROW_DELETE' | 'DATABASE_ROW_INSERT' | 'DATABASE_COLUMN_UPDATE' | 'WEBHOOK';
1508
+ type MutationType = 'INSERT' | 'UPDATE' | 'DELETE';
1509
+ interface SqlMutationData {
1510
+ mutationType: MutationType;
1511
+ requesterDetails: RequesterDetails;
1512
+ }
1513
+ interface DatabaseActionData {
1514
+ tableName: string;
1515
+ requesterDetails: RequesterDetails;
1516
+ }
1517
+ interface ActionRowInsertData<T = DynamicObject> extends DatabaseActionData {
1518
+ insertId: number;
1519
+ insertObject: T;
1520
+ }
1521
+ interface ActionRowDeleteData extends DatabaseActionData {
1522
+ deletedRow: DynamicObject;
1523
+ }
1524
+ interface ActionColumnChangeData extends DatabaseActionData {
1525
+ tableName: string;
1526
+ rowId: number;
1527
+ newData: DynamicObject;
1528
+ oldData: DynamicObject;
1529
+ }
1530
+ interface ActionRowInsertFilter {
1531
+ tableName: string;
1532
+ }
1533
+ interface ActionRowDeleteFilter {
1534
+ tableName: string;
1535
+ }
1536
+ interface ActionColumnChangeFilter {
1537
+ tableName: string;
1538
+ }
1539
+ type TriggerResult = {
1540
+ table: string;
1541
+ insertId?: number;
1542
+ query: string;
1543
+ record: DynamicObject;
1544
+ previousRecord: DynamicObject;
1545
+ requesterId: number;
1546
+ };
1547
+
1548
+ export { type ActionColumnChangeData, type ActionColumnChangeFilter, type ActionRowDeleteData, type ActionRowDeleteFilter, type ActionRowInsertData, type ActionRowInsertFilter, type ApiMethod, type AsyncExpressApplication, type AuthenticateHandler, type AuthenticationUserDetails, type ConjunctionTypes, type DatabaseActionData, type DynamicObject, type ErrorCode, type EventType, HtmlStatusCodes, type LoginDetails, type MatchTypes, type MutationType, type PageQuery, PsqlPool, type RequesterDetails, type ResponseType, type ResponseTypeMap, RsError, type RsErrorData, type RsErrorInternalData, type RsHeaders, type RsPagedResponseData, type RsRequest, type RsResponse, type RsResponseData, type RsRouteHandler, SQL, type SchemaChangeValue, type SchemaPreview, type SqlMutationData, type StandardOrderTypes, type TriggerResult, type ValidAuthenticationCallback, type ValidatorString, escapeColumnName, insertObjectQuery, isValueNumber, logger, questionMarksToOrderedParams, restura, updateObjectQuery };
package/dist/index.js CHANGED
@@ -68,9 +68,7 @@ var __decorateClass = (decorators, target, key, kind) => {
68
68
  var src_exports = {};
69
69
  __export(src_exports, {
70
70
  HtmlStatusCodes: () => HtmlStatusCodes,
71
- PsqlConnection: () => PsqlConnection,
72
71
  PsqlPool: () => PsqlPool,
73
- PsqlTransaction: () => PsqlTransaction,
74
72
  RsError: () => RsError,
75
73
  SQL: () => SQL,
76
74
  escapeColumnName: () => escapeColumnName,
@@ -260,7 +258,7 @@ var import_crypto = require("crypto");
260
258
  var express = __toESM(require("express"));
261
259
  var import_fs3 = __toESM(require("fs"));
262
260
  var import_path3 = __toESM(require("path"));
263
- var import_pg4 = __toESM(require("pg"));
261
+ var import_pg3 = __toESM(require("pg"));
264
262
  var prettier3 = __toESM(require("prettier"));
265
263
 
266
264
  // src/restura/sql/SqlUtils.ts
@@ -1279,7 +1277,8 @@ function insertObjectQuery(table, obj) {
1279
1277
  const params = Object.values(obj);
1280
1278
  const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1281
1279
  const values = params.map((value) => SQL`${value}`).join(", ");
1282
- const query = `INSERT INTO "${table}" (${columns})
1280
+ const query = `
1281
+ INSERT INTO "${table}" (${columns})
1283
1282
  VALUES (${values})
1284
1283
  RETURNING *`;
1285
1284
  return query;
@@ -1289,7 +1288,8 @@ function updateObjectQuery(table, obj, whereStatement) {
1289
1288
  for (const i in obj) {
1290
1289
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1291
1290
  }
1292
- return `UPDATE ${escapeColumnName(table)}
1291
+ return `
1292
+ UPDATE ${escapeColumnName(table)}
1293
1293
  SET ${setArray.join(", ")} ${whereStatement}
1294
1294
  RETURNING *`;
1295
1295
  }
@@ -1319,8 +1319,10 @@ var PsqlConnection = class {
1319
1319
  async queryOne(query, options, requesterDetails) {
1320
1320
  const formattedQuery = questionMarksToOrderedParams(query);
1321
1321
  this.logSqlStatement(formattedQuery, options, requesterDetails);
1322
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(requesterDetails)})
1323
+ `;
1322
1324
  try {
1323
- const response = await this.query(formattedQuery, options);
1325
+ const response = await this.query(queryMetadata + formattedQuery, options);
1324
1326
  if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1325
1327
  else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1326
1328
  return response.rows[0];
@@ -1337,8 +1339,10 @@ var PsqlConnection = class {
1337
1339
  async runQuery(query, options, requesterDetails) {
1338
1340
  const formattedQuery = questionMarksToOrderedParams(query);
1339
1341
  this.logSqlStatement(formattedQuery, options, requesterDetails);
1342
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(requesterDetails)})
1343
+ `;
1340
1344
  try {
1341
- const response = await this.query(formattedQuery, options);
1345
+ const response = await this.query(queryMetadata + formattedQuery, options);
1342
1346
  return response.rows;
1343
1347
  } catch (error) {
1344
1348
  console.error(error, query, options);
@@ -1589,6 +1593,110 @@ var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
1589
1593
  });
1590
1594
  var filterPsqlParser_default = filterPsqlParser;
1591
1595
 
1596
+ // src/restura/eventManager.ts
1597
+ var import_bluebird = __toESM(require("bluebird"));
1598
+ var EventManager = class {
1599
+ constructor() {
1600
+ this.actionHandlers = {
1601
+ DATABASE_ROW_DELETE: [],
1602
+ DATABASE_ROW_INSERT: [],
1603
+ DATABASE_COLUMN_UPDATE: []
1604
+ };
1605
+ }
1606
+ addRowInsertHandler(onInsert, filter) {
1607
+ this.actionHandlers.DATABASE_ROW_INSERT.push({
1608
+ callback: onInsert,
1609
+ filter
1610
+ });
1611
+ }
1612
+ addColumnChangeHandler(onUpdate, filter) {
1613
+ this.actionHandlers.DATABASE_COLUMN_UPDATE.push({
1614
+ callback: onUpdate,
1615
+ filter
1616
+ });
1617
+ }
1618
+ addRowDeleteHandler(onDelete, filter) {
1619
+ this.actionHandlers.DATABASE_ROW_DELETE.push({
1620
+ callback: onDelete,
1621
+ filter
1622
+ });
1623
+ }
1624
+ async fireActionFromDbTrigger(sqlMutationData, result) {
1625
+ if (sqlMutationData.mutationType === "INSERT") {
1626
+ await this.fireInsertActions(sqlMutationData, result);
1627
+ } else if (sqlMutationData.mutationType === "UPDATE") {
1628
+ await this.fireUpdateActions(sqlMutationData, result);
1629
+ } else if (sqlMutationData.mutationType === "DELETE") {
1630
+ await this.fireDeleteActions(sqlMutationData, result);
1631
+ }
1632
+ }
1633
+ async fireInsertActions(data, triggerResult) {
1634
+ await import_bluebird.default.map(
1635
+ this.actionHandlers.DATABASE_ROW_INSERT,
1636
+ ({ callback, filter }) => {
1637
+ if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
1638
+ const insertData = {
1639
+ tableName: triggerResult.table,
1640
+ insertId: triggerResult.record.id,
1641
+ insertObject: triggerResult.record,
1642
+ requesterDetails: data.requesterDetails
1643
+ };
1644
+ callback(insertData, data.requesterDetails);
1645
+ },
1646
+ { concurrency: 10 }
1647
+ );
1648
+ }
1649
+ async fireDeleteActions(data, triggerResult) {
1650
+ await import_bluebird.default.map(
1651
+ this.actionHandlers.DATABASE_ROW_DELETE,
1652
+ ({ callback, filter }) => {
1653
+ if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
1654
+ const deleteData = {
1655
+ tableName: triggerResult.table,
1656
+ deletedRow: triggerResult.previousRecord,
1657
+ requesterDetails: data.requesterDetails
1658
+ };
1659
+ callback(deleteData, data.requesterDetails);
1660
+ },
1661
+ { concurrency: 10 }
1662
+ );
1663
+ }
1664
+ async fireUpdateActions(data, triggerResult) {
1665
+ await import_bluebird.default.map(
1666
+ this.actionHandlers.DATABASE_COLUMN_UPDATE,
1667
+ ({ callback, filter }) => {
1668
+ if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
1669
+ const columnChangeData = {
1670
+ tableName: triggerResult.table,
1671
+ rowId: triggerResult.record.id,
1672
+ newData: triggerResult.record,
1673
+ oldData: triggerResult.previousRecord,
1674
+ requesterDetails: data.requesterDetails
1675
+ };
1676
+ callback(columnChangeData, data.requesterDetails);
1677
+ },
1678
+ { concurrency: 10 }
1679
+ );
1680
+ }
1681
+ hasHandlersForEventType(eventType, filter, triggerResult) {
1682
+ if (filter) {
1683
+ switch (eventType) {
1684
+ case "DATABASE_ROW_INSERT":
1685
+ case "DATABASE_ROW_DELETE":
1686
+ if (filter.tableName && filter.tableName !== triggerResult.table) return false;
1687
+ break;
1688
+ case "DATABASE_COLUMN_UPDATE":
1689
+ const filterColumnChange = filter;
1690
+ if (filterColumnChange.tableName !== filter.tableName) return false;
1691
+ break;
1692
+ }
1693
+ }
1694
+ return true;
1695
+ }
1696
+ };
1697
+ var eventManager = new EventManager();
1698
+ var eventManager_default = eventManager;
1699
+
1592
1700
  // src/restura/sql/PsqlEngine.ts
1593
1701
  var { Client } = import_pg2.default;
1594
1702
  var systemUser = {
@@ -1598,9 +1706,49 @@ var systemUser = {
1598
1706
  isSystemUser: true
1599
1707
  };
1600
1708
  var PsqlEngine = class extends SqlEngine {
1601
- constructor(psqlConnectionPool) {
1709
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1602
1710
  super();
1603
1711
  this.psqlConnectionPool = psqlConnectionPool;
1712
+ if (shouldListenForDbTriggers) {
1713
+ this.setupTriggerListeners = this.listenForDbTriggers();
1714
+ }
1715
+ }
1716
+ async close() {
1717
+ if (this.triggerClient) {
1718
+ await this.triggerClient.end();
1719
+ }
1720
+ }
1721
+ async listenForDbTriggers() {
1722
+ this.triggerClient = new Client({
1723
+ user: this.psqlConnectionPool.poolConfig.user,
1724
+ host: this.psqlConnectionPool.poolConfig.host,
1725
+ database: this.psqlConnectionPool.poolConfig.database,
1726
+ password: this.psqlConnectionPool.poolConfig.password,
1727
+ port: this.psqlConnectionPool.poolConfig.port,
1728
+ connectionTimeoutMillis: 2e3
1729
+ });
1730
+ await this.triggerClient.connect();
1731
+ const promises = [];
1732
+ promises.push(this.triggerClient.query("LISTEN insert"));
1733
+ promises.push(this.triggerClient.query("LISTEN update"));
1734
+ promises.push(this.triggerClient.query("LISTEN delete"));
1735
+ await Promise.all(promises);
1736
+ this.triggerClient.on("notification", async (msg) => {
1737
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1738
+ const payload = JSON.parse(msg.payload);
1739
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1740
+ }
1741
+ });
1742
+ }
1743
+ async handleTrigger(payload, mutationType) {
1744
+ const findRequesterDetailsRegex = /^--QUERY_METADATA\(\{.*\}\)/;
1745
+ let requesterDetails = {};
1746
+ const match = payload.query.match(findRequesterDetailsRegex);
1747
+ if (match) {
1748
+ const jsonString = match[0].slice(match[0].indexOf("{"), match[0].lastIndexOf("}") + 1);
1749
+ requesterDetails = import_core_utils5.ObjectUtils.safeParse(jsonString);
1750
+ await eventManager_default.fireActionFromDbTrigger({ requesterDetails, mutationType }, payload);
1751
+ }
1604
1752
  }
1605
1753
  async createDatabaseFromSchema(schema, connection) {
1606
1754
  const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
@@ -1611,7 +1759,11 @@ var PsqlEngine = class extends SqlEngine {
1611
1759
  const sqlStatements = [];
1612
1760
  const enums = [];
1613
1761
  const indexes = [];
1762
+ const triggers = [];
1614
1763
  for (const table of schema.database) {
1764
+ triggers.push(this.createInsertTriggers(table.name));
1765
+ triggers.push(this.createUpdateTrigger(table.name));
1766
+ triggers.push(this.createDeleteTrigger(table.name));
1615
1767
  let sql = `CREATE TABLE "${table.name}"
1616
1768
  ( `;
1617
1769
  const tableColumns = [];
@@ -1649,7 +1801,7 @@ var PsqlEngine = class extends SqlEngine {
1649
1801
  indexes.push(
1650
1802
  ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
1651
1803
  return `"${item}" ${index.order}`;
1652
- }).join(", ")})`
1804
+ }).join(", ")});`
1653
1805
  );
1654
1806
  }
1655
1807
  }
@@ -1679,7 +1831,8 @@ var PsqlEngine = class extends SqlEngine {
1679
1831
  }
1680
1832
  sqlStatements.push(sql + constraints.join(",\n") + ";");
1681
1833
  }
1682
- sqlStatements.push(indexes.join(";\n"));
1834
+ sqlStatements.push(indexes.join("\n"));
1835
+ sqlStatements.push(triggers.join("\n"));
1683
1836
  return enums.join("\n") + "\n" + sqlStatements.join("\n\n");
1684
1837
  }
1685
1838
  async getScratchPool() {
@@ -1747,8 +1900,7 @@ var PsqlEngine = class extends SqlEngine {
1747
1900
  )) {
1748
1901
  return "'[]'";
1749
1902
  }
1750
- return `COALESCE((
1751
- SELECT JSON_AGG(JSON_BUILD_OBJECT(
1903
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1752
1904
  ${item.subquery.properties.map((nestedItem) => {
1753
1905
  if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1754
1906
  ...routeData.joins,
@@ -1757,7 +1909,7 @@ var PsqlEngine = class extends SqlEngine {
1757
1909
  return;
1758
1910
  }
1759
1911
  if (nestedItem.subquery) {
1760
- return `"${nestedItem.name}", ${this.createNestedSelect(
1912
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1761
1913
  // recursion
1762
1914
  req,
1763
1915
  schema,
@@ -1768,7 +1920,7 @@ var PsqlEngine = class extends SqlEngine {
1768
1920
  )}`;
1769
1921
  }
1770
1922
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1771
- }).filter(Boolean).join(",")}
1923
+ }).filter(Boolean).join(", ")}
1772
1924
  ))
1773
1925
  FROM
1774
1926
  "${item.subquery.table}"
@@ -1896,10 +2048,12 @@ var PsqlEngine = class extends SqlEngine {
1896
2048
  req.requesterDetails.role,
1897
2049
  sqlParams
1898
2050
  );
1899
- let deleteStatement = `DELETE
1900
- FROM "${routeData.table}" ${joinStatement}`;
1901
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1902
- deleteStatement += ";";
2051
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2052
+ if (whereClause.replace(/\s/g, "") === "") {
2053
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2054
+ }
2055
+ const deleteStatement = `
2056
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
1903
2057
  await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1904
2058
  return true;
1905
2059
  }
@@ -1962,17 +2116,17 @@ var PsqlEngine = class extends SqlEngine {
1962
2116
  );
1963
2117
  let operator = item.operator;
1964
2118
  if (operator === "LIKE") {
1965
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2119
+ item.value = `%${item.value}%`;
1966
2120
  } else if (operator === "STARTS WITH") {
1967
2121
  operator = "LIKE";
1968
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2122
+ item.value = `${item.value}%`;
1969
2123
  } else if (operator === "ENDS WITH") {
1970
2124
  operator = "LIKE";
1971
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2125
+ item.value = `%${item.value}`;
1972
2126
  }
1973
2127
  const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
1974
2128
  const escapedValue = SQL`${replacedValue}`;
1975
- whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
2129
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
1976
2130
  `;
1977
2131
  });
1978
2132
  const data = req.data;
@@ -2006,7 +2160,67 @@ var PsqlEngine = class extends SqlEngine {
2006
2160
  }
2007
2161
  return whereClause;
2008
2162
  }
2163
+ createUpdateTrigger(tableName) {
2164
+ return `
2165
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2166
+ RETURNS TRIGGER AS $$
2167
+ BEGIN
2168
+ PERFORM pg_notify('update', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2169
+ RETURN NEW;
2170
+ END;
2171
+ $$ LANGUAGE plpgsql;
2172
+
2173
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2174
+ AFTER UPDATE ON "${tableName}"
2175
+ FOR EACH ROW
2176
+ EXECUTE FUNCTION notify_${tableName}_update();
2177
+ `;
2178
+ }
2179
+ createDeleteTrigger(tableName) {
2180
+ return `
2181
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2182
+ RETURNS TRIGGER AS $$
2183
+ BEGIN
2184
+ PERFORM pg_notify('delete', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2185
+ RETURN NEW;
2186
+ END;
2187
+ $$ LANGUAGE plpgsql;
2188
+
2189
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2190
+ AFTER DELETE ON "${tableName}"
2191
+ FOR EACH ROW
2192
+ EXECUTE FUNCTION notify_${tableName}_delete();
2193
+ `;
2194
+ }
2195
+ createInsertTriggers(tableName) {
2196
+ return `
2197
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2198
+ RETURNS TRIGGER AS $$
2199
+ BEGIN
2200
+ PERFORM pg_notify('insert', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2201
+ RETURN NEW;
2202
+ END;
2203
+ $$ LANGUAGE plpgsql;
2204
+
2205
+ CREATE TRIGGER "${tableName}_insert"
2206
+ AFTER INSERT ON "${tableName}"
2207
+ FOR EACH ROW
2208
+ EXECUTE FUNCTION notify_${tableName}_insert();
2209
+ `;
2210
+ }
2009
2211
  };
2212
+ __decorateClass([
2213
+ boundMethod
2214
+ ], PsqlEngine.prototype, "handleTrigger", 1);
2215
+ __decorateClass([
2216
+ boundMethod
2217
+ ], PsqlEngine.prototype, "createUpdateTrigger", 1);
2218
+ __decorateClass([
2219
+ boundMethod
2220
+ ], PsqlEngine.prototype, "createDeleteTrigger", 1);
2221
+ __decorateClass([
2222
+ boundMethod
2223
+ ], PsqlEngine.prototype, "createInsertTriggers", 1);
2010
2224
  function schemaToPsqlType(column, tableName) {
2011
2225
  if (column.hasAutoIncrement) return "BIGSERIAL";
2012
2226
  if (column.type === "ENUM") return `"${tableName}_${column.name}_enum"`;
@@ -2015,35 +2229,6 @@ function schemaToPsqlType(column, tableName) {
2015
2229
  return column.type;
2016
2230
  }
2017
2231
 
2018
- // src/restura/sql/PsqlTransaction.ts
2019
- var import_pg3 = __toESM(require("pg"));
2020
- var { Client: Client2 } = import_pg3.default;
2021
- var PsqlTransaction = class extends PsqlConnection {
2022
- constructor(clientConfig) {
2023
- super();
2024
- this.clientConfig = clientConfig;
2025
- this.client = new Client2(clientConfig);
2026
- this.beginTransactionPromise = this.beginTransaction();
2027
- }
2028
- async beginTransaction() {
2029
- return this.query("BEGIN");
2030
- }
2031
- async rollback() {
2032
- return this.query("ROLLBACK");
2033
- }
2034
- async commit() {
2035
- return this.query("COMMIT");
2036
- }
2037
- async release() {
2038
- return this.client.end();
2039
- }
2040
- async query(query, values) {
2041
- await this.client.connect();
2042
- await this.beginTransactionPromise;
2043
- return this.client.query(query, values);
2044
- }
2045
- };
2046
-
2047
2232
  // src/restura/compareSchema.ts
2048
2233
  var import_lodash = __toESM(require("lodash.clonedeep"));
2049
2234
  var CompareSchema = class {
@@ -2135,7 +2320,7 @@ var compareSchema = new CompareSchema();
2135
2320
  var compareSchema_default = compareSchema;
2136
2321
 
2137
2322
  // src/restura/restura.ts
2138
- var { types } = import_pg4.default;
2323
+ var { types } = import_pg3.default;
2139
2324
  var ResturaEngine = class {
2140
2325
  constructor() {
2141
2326
  this.publicEndpoints = {
@@ -2510,12 +2695,14 @@ var setupPgReturnTypes = () => {
2510
2695
  };
2511
2696
  setupPgReturnTypes();
2512
2697
  var restura = new ResturaEngine();
2698
+
2699
+ // src/restura/sql/PsqlTransaction.ts
2700
+ var import_pg4 = __toESM(require("pg"));
2701
+ var { Client: Client2 } = import_pg4.default;
2513
2702
  // Annotate the CommonJS export names for ESM import in node:
2514
2703
  0 && (module.exports = {
2515
2704
  HtmlStatusCodes,
2516
- PsqlConnection,
2517
2705
  PsqlPool,
2518
- PsqlTransaction,
2519
2706
  RsError,
2520
2707
  SQL,
2521
2708
  escapeColumnName,