@restura/core 2.1.0 → 2.1.2

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.ts CHANGED
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import * as express from 'express';
4
4
  import { IncomingHttpHeaders } from 'http2';
5
5
  import { QueryResultRow, QueryConfigValues, QueryResult, PoolConfig, Pool, ClientConfig, Client } from 'pg';
6
- import peg from 'pegjs';
6
+ import * as pegjs from 'pegjs';
7
7
 
8
8
  type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
9
9
  interface ResturaLogger {
@@ -1155,7 +1155,7 @@ declare class ResturaEngine {
1155
1155
  }
1156
1156
  declare const restura: ResturaEngine;
1157
1157
 
1158
- declare const filterPsqlParser: peg.Parser;
1158
+ declare const filterPsqlParser: pegjs.Parser;
1159
1159
 
1160
1160
  declare abstract class SqlEngine {
1161
1161
  runQueryForRoute(req: RsRequest<unknown>, routeData: StandardRouteData, schema: ResturaSchema): Promise<DynamicObject | any[] | boolean>;
@@ -1358,6 +1358,7 @@ interface TriggerSqlOptions {
1358
1358
  delivery?: 'direct' | 'outbox';
1359
1359
  channel?: string;
1360
1360
  tableColumns?: string[];
1361
+ columnTypes?: Record<string, string>;
1361
1362
  }
1362
1363
  interface SchemaGenerationOptions {
1363
1364
  eventDelivery?: 'direct' | 'outbox';
package/dist/index.js CHANGED
@@ -259,7 +259,7 @@ var SqlUtils = class _SqlUtils {
259
259
  if (type.startsWith("decimal") || type.startsWith("numeric")) return "string";
260
260
  if (type.indexOf("int") > -1 || type.startsWith("double") || type.startsWith("float") || type.indexOf("serial") > -1 || type.startsWith("real") || type.startsWith("double precision"))
261
261
  return "number";
262
- if (type === "json") {
262
+ if (type === "json" || type === "jsonb") {
263
263
  if (!value) return "object";
264
264
  return value.split(",").map((val) => {
265
265
  return val.replace(/['"]/g, "");
@@ -2359,23 +2359,31 @@ CREATE INDEX IF NOT EXISTS "${OUTBOX_TABLE_NAME}_processedOn_index" ON "${OUTBOX
2359
2359
  }
2360
2360
  function resolveNotifyColumns(tableName, notify, tableColumns) {
2361
2361
  if (!notify) return [];
2362
+ let columns;
2362
2363
  if (notify === "ALL") {
2363
2364
  if (!tableColumns) {
2364
2365
  throw new Error(
2365
2366
  `notify: "ALL" on table "${tableName}" requires tableColumns so the sensitive-column denylist can be applied. Pass options.tableColumns or list columns explicitly.`
2366
2367
  );
2367
2368
  }
2368
- return tableColumns.filter((column) => !isSensitiveColumnName(column));
2369
+ columns = tableColumns.filter((column) => !isSensitiveColumnName(column));
2370
+ } else {
2371
+ columns = notify.map((entry) => {
2372
+ if (entry.startsWith("!")) return entry.slice(1);
2373
+ if (isSensitiveColumnName(entry)) {
2374
+ throw new Error(
2375
+ `Refusing to include sensitive column "${tableName}"."${entry}" in notify trigger payloads. Remove it from the notify list or prefix it with '!' to force-include it.`
2376
+ );
2377
+ }
2378
+ return entry;
2379
+ });
2369
2380
  }
2370
- return notify.map((entry) => {
2371
- if (entry.startsWith("!")) return entry.slice(1);
2372
- if (isSensitiveColumnName(entry)) {
2373
- throw new Error(
2374
- `Refusing to include sensitive column "${tableName}"."${entry}" in notify trigger payloads. Remove it from the notify list or prefix it with '!' to force-include it.`
2375
- );
2376
- }
2377
- return entry;
2378
- });
2381
+ if (!columns.length) {
2382
+ throw new Error(
2383
+ `notify config on table "${tableName}" resolves to no columns \u2014 remove the notify key or list at least one column.`
2384
+ );
2385
+ }
2386
+ return columns;
2379
2387
  }
2380
2388
  function sqlStringLiteral(value) {
2381
2389
  return value.replace(/'/g, "''");
@@ -2425,7 +2433,14 @@ function buildTriggerFunctionSql(tableName, operation, notify, options) {
2425
2433
  const outboxDeclare = delivery === "outbox" ? "\n outbox_id BIGINT;" : "";
2426
2434
  const legacyDrop = operation === "update" && tableName !== tableName.toLowerCase() ? `DROP TRIGGER IF EXISTS ${tableName}_update ON "${tableName}";
2427
2435
  ` : "";
2428
- const updateGuard = operation === "update" ? "\n WHEN (OLD.* IS DISTINCT FROM NEW.*)" : "";
2436
+ let updateScope = "";
2437
+ let updateGuard = "";
2438
+ if (operation === "update") {
2439
+ updateScope = ` OF ${columns.map((column) => `"${column}"`).join(", ")}`;
2440
+ const columnComparison = (column) => options?.columnTypes?.[column] === "JSON" ? `OLD."${column}"::jsonb IS DISTINCT FROM NEW."${column}"::jsonb` : `OLD."${column}" IS DISTINCT FROM NEW."${column}"`;
2441
+ updateGuard = `
2442
+ WHEN (${columns.map(columnComparison).join(" OR ")})`;
2443
+ }
2429
2444
  return `
2430
2445
  CREATE OR REPLACE FUNCTION ${functionName}()
2431
2446
  RETURNS TRIGGER AS $$
@@ -2440,7 +2455,7 @@ END;
2440
2455
  $$ LANGUAGE plpgsql;
2441
2456
 
2442
2457
  ${legacyDrop}CREATE OR REPLACE TRIGGER "${tableName}_${operation}"
2443
- AFTER ${operation.toUpperCase()} ON "${tableName}"
2458
+ AFTER ${operation.toUpperCase()}${updateScope} ON "${tableName}"
2444
2459
  FOR EACH ROW${updateGuard}
2445
2460
  EXECUTE FUNCTION ${functionName}();
2446
2461
  `;
@@ -2465,7 +2480,8 @@ function generateNotifyTriggersSql(schema, options) {
2465
2480
  const triggerOptions = {
2466
2481
  delivery: options?.eventDelivery,
2467
2482
  channel: options?.outboxChannel,
2468
- tableColumns: table.columns.map((column) => column.name)
2483
+ tableColumns: table.columns.map((column) => column.name),
2484
+ columnTypes: Object.fromEntries(table.columns.map((column) => [column.name, column.type]))
2469
2485
  };
2470
2486
  statements.push(createInsertTriggerSql(table.name, table.notify, triggerOptions));
2471
2487
  statements.push(createUpdateTriggerSql(table.name, table.notify, triggerOptions));
@@ -2796,7 +2812,26 @@ var EventOutboxConsumer = class {
2796
2812
  };
2797
2813
 
2798
2814
  // src/restura/sql/filterPsqlParser.ts
2815
+ import format2 from "pg-format";
2816
+
2817
+ // src/restura/sql/compilePegParser.ts
2799
2818
  import peg from "pegjs";
2819
+ function compilePegParser(grammar, dependencies, modules) {
2820
+ const parserSource = peg.generate(grammar, {
2821
+ output: "source",
2822
+ format: "commonjs",
2823
+ dependencies
2824
+ });
2825
+ const moduleShim = { exports: {} };
2826
+ const requireShim = (moduleId) => {
2827
+ if (moduleId in modules) return modules[moduleId];
2828
+ throw new Error(`compilePegParser: no module provided for dependency '${moduleId}'`);
2829
+ };
2830
+ new Function("module", "exports", "require", parserSource)(moduleShim, moduleShim.exports, requireShim);
2831
+ return moduleShim.exports;
2832
+ }
2833
+
2834
+ // src/restura/sql/filterPsqlParser.ts
2800
2835
  var initializers = `
2801
2836
  // Quotes a SQL identifier (column/table name) with double quotes, escaping any embedded quotes
2802
2837
  function quoteSqlIdentity(value) {
@@ -3132,10 +3167,7 @@ ValueWithPipesChar
3132
3167
  / c:":" !":" { return c; }
3133
3168
  `;
3134
3169
  var fullGrammar = entryGrammar + oldGrammar + newGrammar;
3135
- var filterPsqlParser = peg.generate(fullGrammar, {
3136
- format: "commonjs",
3137
- dependencies: { format: "pg-format" }
3138
- });
3170
+ var filterPsqlParser = compilePegParser(fullGrammar, { format: "pg-format" }, { "pg-format": format2 });
3139
3171
  var filterPsqlParser_default = filterPsqlParser;
3140
3172
 
3141
3173
  // src/restura/sql/PsqlEngine.ts