prostgles-server 4.2.606 → 4.2.607

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.
Files changed (116) hide show
  1. package/README.md +29 -0
  2. package/dist/Audit/AuditTable.d.ts +18 -0
  3. package/dist/Audit/AuditTable.d.ts.map +1 -0
  4. package/dist/Audit/AuditTable.js +18 -0
  5. package/dist/Audit/AuditTable.js.map +1 -0
  6. package/dist/Audit/AuditTypes.d.ts +25 -0
  7. package/dist/Audit/AuditTypes.d.ts.map +1 -0
  8. package/dist/Audit/AuditTypes.js +3 -0
  9. package/dist/Audit/AuditTypes.js.map +1 -0
  10. package/dist/Audit/getAuditTableConfig.d.ts +13 -0
  11. package/dist/Audit/getAuditTableConfig.d.ts.map +1 -0
  12. package/dist/Audit/getAuditTableConfig.js +54 -0
  13. package/dist/Audit/getAuditTableConfig.js.map +1 -0
  14. package/dist/Audit/parseAuditConfig.d.ts +5 -0
  15. package/dist/Audit/parseAuditConfig.d.ts.map +1 -0
  16. package/dist/Audit/parseAuditConfig.js +123 -0
  17. package/dist/Audit/parseAuditConfig.js.map +1 -0
  18. package/dist/DboBuilder/DboBuilder.js +2 -2
  19. package/dist/DboBuilder/DboBuilder.js.map +1 -1
  20. package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts +1 -1
  21. package/dist/DboBuilder/runSql/QueryStreamer.d.ts.map +1 -1
  22. package/dist/DboBuilder/runSql/QueryStreamer.js +1 -0
  23. package/dist/DboBuilder/runSql/QueryStreamer.js.map +1 -1
  24. package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.d.ts +3 -3
  25. package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.d.ts.map +1 -1
  26. package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.js +2 -2
  27. package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.js.map +1 -1
  28. package/dist/DboBuilder/schema/getViewReferenceColumns.d.ts +2 -2
  29. package/dist/DboBuilder/schema/getViewReferenceColumns.d.ts.map +1 -1
  30. package/dist/DboBuilder/schema/getViewReferenceColumns.js +4 -2
  31. package/dist/DboBuilder/schema/getViewReferenceColumns.js.map +1 -1
  32. package/dist/Prostgles.d.ts +6 -0
  33. package/dist/Prostgles.d.ts.map +1 -1
  34. package/dist/Prostgles.js +33 -5
  35. package/dist/Prostgles.js.map +1 -1
  36. package/dist/ProstglesTypes.d.ts +3 -0
  37. package/dist/ProstglesTypes.d.ts.map +1 -1
  38. package/dist/ProstglesTypes.js.map +1 -1
  39. package/dist/PubSubManager/PubSubManager.d.ts +1 -1
  40. package/dist/PubSubManager/getCreatePubSubManagerError.js +1 -1
  41. package/dist/PubSubManager/getCreatePubSubManagerError.js.map +1 -1
  42. package/dist/PubSubManager/init/getPubSubManagerInitQuery.js +4 -4
  43. package/dist/PubSubManager/init/getPubSubManagerInitQuery.js.map +1 -1
  44. package/dist/PublishParser/getTableRulesWithoutFileTable.d.ts.map +1 -1
  45. package/dist/PublishParser/getTableRulesWithoutFileTable.js +5 -1
  46. package/dist/PublishParser/getTableRulesWithoutFileTable.js.map +1 -1
  47. package/dist/TableConfig/TableConfigTypes.d.ts +1 -1
  48. package/dist/TableConfig/TableConfigTypes.d.ts.map +1 -1
  49. package/dist/TableConfig/TableConfigurator.js +3 -3
  50. package/dist/TableConfig/TableConfigurator.js.map +1 -1
  51. package/dist/TableConfig/getTableConfigSchemaQueries.d.ts.map +1 -1
  52. package/dist/TableConfig/getTableConfigSchemaQueries.js +3 -2
  53. package/dist/TableConfig/getTableConfigSchemaQueries.js.map +1 -1
  54. package/dist/TableConfig/getTableTriggerQueries.d.ts +2 -2
  55. package/dist/TableConfig/getTableTriggerQueries.d.ts.map +1 -1
  56. package/dist/TableConfig/getTableTriggerQueries.js +57 -34
  57. package/dist/TableConfig/getTableTriggerQueries.js.map +1 -1
  58. package/dist/TableConfig/initTableConfig.d.ts.map +1 -1
  59. package/dist/TableConfig/initTableConfig.js +5 -6
  60. package/dist/TableConfig/initTableConfig.js.map +1 -1
  61. package/dist/TableConfig/managedTriggerNames.d.ts +8 -0
  62. package/dist/TableConfig/managedTriggerNames.d.ts.map +1 -0
  63. package/dist/TableConfig/managedTriggerNames.js +17 -0
  64. package/dist/TableConfig/managedTriggerNames.js.map +1 -0
  65. package/dist/TableConfig/runSQLFile.js +1 -1
  66. package/dist/TableConfig/runSQLFile.js.map +1 -1
  67. package/dist/TableConfig/syncTableTriggers.d.ts +4 -0
  68. package/dist/TableConfig/syncTableTriggers.d.ts.map +1 -0
  69. package/dist/TableConfig/syncTableTriggers.js +32 -0
  70. package/dist/TableConfig/syncTableTriggers.js.map +1 -0
  71. package/dist/deferDbQueries.d.ts +4 -0
  72. package/dist/deferDbQueries.d.ts.map +1 -0
  73. package/dist/deferDbQueries.js +24 -0
  74. package/dist/deferDbQueries.js.map +1 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +4 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/initProstgles.d.ts +1 -1
  80. package/dist/initProstgles.d.ts.map +1 -1
  81. package/dist/initProstgles.js +24 -13
  82. package/dist/initProstgles.js.map +1 -1
  83. package/dist/updateConfiguration.d.ts.map +1 -1
  84. package/dist/updateConfiguration.js +1 -6
  85. package/dist/updateConfiguration.js.map +1 -1
  86. package/lib/Audit/AuditTable.ts +25 -0
  87. package/lib/Audit/AuditTypes.ts +35 -0
  88. package/lib/Audit/getAuditTableConfig.ts +76 -0
  89. package/lib/Audit/parseAuditConfig.ts +158 -0
  90. package/lib/DboBuilder/DboBuilder.ts +2 -2
  91. package/lib/DboBuilder/runSql/QueryStreamer.ts +1 -0
  92. package/lib/DboBuilder/schema/getTablesForSchemaPostgresSQL.ts +2 -2
  93. package/lib/DboBuilder/schema/getViewReferenceColumns.ts +7 -8
  94. package/lib/Prostgles.ts +34 -5
  95. package/lib/ProstglesTypes.ts +3 -0
  96. package/lib/PubSubManager/getCreatePubSubManagerError.ts +1 -1
  97. package/lib/PubSubManager/init/getPubSubManagerInitQuery.ts +4 -4
  98. package/lib/PublishParser/getTableRulesWithoutFileTable.ts +6 -1
  99. package/lib/TableConfig/TableConfigTypes.ts +1 -1
  100. package/lib/TableConfig/TableConfigurator.ts +2 -2
  101. package/lib/TableConfig/getTableConfigSchemaQueries.ts +3 -2
  102. package/lib/TableConfig/getTableTriggerQueries.ts +85 -41
  103. package/lib/TableConfig/initTableConfig.ts +4 -7
  104. package/lib/TableConfig/managedTriggerNames.ts +16 -0
  105. package/lib/TableConfig/runSQLFile.ts +1 -1
  106. package/lib/TableConfig/syncTableTriggers.ts +42 -0
  107. package/lib/deferDbQueries.ts +27 -0
  108. package/lib/index.ts +3 -0
  109. package/lib/initProstgles.ts +24 -12
  110. package/lib/updateConfiguration.ts +1 -6
  111. package/package.json +1 -1
  112. package/dist/TableConfig/getTableTriggerQueries.spec.d.ts +0 -2
  113. package/dist/TableConfig/getTableTriggerQueries.spec.d.ts.map +0 -1
  114. package/dist/TableConfig/getTableTriggerQueries.spec.js +0 -56
  115. package/dist/TableConfig/getTableTriggerQueries.spec.js.map +0 -1
  116. package/lib/TableConfig/getTableTriggerQueries.spec.ts +0 -77
@@ -1 +1 @@
1
- {"version":3,"file":"updateConfiguration.js","sourceRoot":"","sources":["../lib/updateConfiguration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"updateConfiguration.js","sourceRoot":"","sources":["../lib/updateConfiguration.ts"],"names":[],"mappings":";;;AAAA,qDAAiF;AAI1E,MAAM,mBAAmB,GAAG,KAAK,EACtC,IAAe,EACf,OAA6B,EAC7B,OAAyC,EACzC,KAAY,EACZ,EAAE;IACF,MAAM,kBAAkB,GAAG,IAAA,yBAAO,EAAC,OAAO,CAAC;SACxC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,6DAA6D;QAC7D,IAAI,KAAK,IAAI,CAAC,IAAA,yBAAO,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAChD,YAAY;YACZ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YAC1B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO;IACT,CAAC,CAAC;SACD,MAAM,CAAC,2BAAS,CAAC,CAAC;IACrB,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,CAAC;QAC/B,MAAM,IAAI,CAAC,WAAW,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QACnC,OAAO;IACT,CAAC;IAED,IAAI,IAAA,0BAAQ,EAAC,kBAAkB,EAAE,SAAS,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,WAAW,EAAE,CAAC;IACrB,CAAC;IACD,IAAI,IAAA,0BAAQ,EAAC,kBAAkB,EAAE,QAAQ,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;IAC1B,CAAC;IACD,IAAI,IAAA,0BAAQ,EAAC,kBAAkB,EAAE,MAAM,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,IAAA,0BAAQ,EAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;YACvC,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IACE,IAAA,0BAAQ,EAAC,kBAAkB,EAAE,qBAAqB,CAAC;QACnD,IAAA,0BAAQ,EAAC,kBAAkB,EAAE,+BAA+B,CAAC,EAC7D,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAED,IAAI,IAAA,yBAAO,EAAC,OAAO,CAAC;QAAE,OAAO;IAE7B,IACE,kBAAkB,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,EAChG,CAAC;QACD,OAAO;IACT,CAAC;IAED;;;OAGG;IACH,IACE,kBAAkB,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,4BAAoB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,EAChG,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YACvB,IAAI,EAAE,aAAa;YACnB,OAAO;SACR,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC;AAnEW,QAAA,mBAAmB,uBAmE9B;AAEF;;GAEG;AACU,QAAA,oBAAoB,GAAG;IAClC,IAAI;IACJ,MAAM;IACN,SAAS;IACT,WAAW;IACX,eAAe;IACf,oBAAoB;CACyB,CAAC;AAEhD,MAAM,oBAAoB,GAAG,CAAC,YAAY,CAA+C,CAAC"}
@@ -0,0 +1,25 @@
1
+ export const AUDIT_TABLE_COLUMN_DEFINITIONS = {
2
+ id: "BIGSERIAL PRIMARY KEY",
3
+ created_at: "TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()",
4
+ schema_name: "TEXT NOT NULL",
5
+ table_name: "TEXT NOT NULL",
6
+ entity_type: "TEXT NOT NULL",
7
+ operation:
8
+ "TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE'))",
9
+ old_id: "JSONB",
10
+ new_id: "JSONB",
11
+ old_row: "JSONB",
12
+ new_row: "JSONB",
13
+ actor: "JSONB",
14
+ } as const;
15
+
16
+ export type AuditTableRow = {
17
+ [K in keyof typeof AUDIT_TABLE_COLUMN_DEFINITIONS]: K extends "operation"
18
+ ? "INSERT" | "UPDATE" | "DELETE"
19
+ : (typeof AUDIT_TABLE_COLUMN_DEFINITIONS)[K] extends "JSONB"
20
+ ? Record<string, unknown> | null
21
+ : string;
22
+ };
23
+ export const AUDIT_TABLE_COLUMNS = Object.keys(
24
+ AUDIT_TABLE_COLUMN_DEFINITIONS,
25
+ ) as (keyof AuditTableRow)[];
@@ -0,0 +1,35 @@
1
+ export type SchemaConfigTableName<S> = [S] extends [void]
2
+ ? string
3
+ : Extract<keyof S, string>;
4
+ export type SchemaConfigColumnName<S, T extends SchemaConfigTableName<S>> = [
5
+ S,
6
+ ] extends [void]
7
+ ? string
8
+ : T extends keyof S
9
+ ? S[T] extends { columns: infer C }
10
+ ? Extract<keyof C, string>
11
+ : never
12
+ : never;
13
+
14
+ export type SchemaConfigAuditTableOptions<
15
+ S,
16
+ T extends SchemaConfigTableName<S>,
17
+ > = {
18
+ entityType?: string;
19
+ /** Defaults to all primary-key columns. Required for tables without a primary key. */
20
+ idColumns?: readonly SchemaConfigColumnName<S, T>[];
21
+ excludeColumns?: readonly SchemaConfigColumnName<S, T>[];
22
+ };
23
+ export type SchemaConfigAuditIncludedTables<S> = {
24
+ [T in SchemaConfigTableName<S>]?: 1 | SchemaConfigAuditTableOptions<S, T>;
25
+ };
26
+ export type SchemaConfigAuditExcludedTables<S> = {
27
+ [T in SchemaConfigTableName<S>]?: 0;
28
+ };
29
+ export type SchemaConfigAudit<S = void> = {
30
+ /** Managed append-only table. Uses the same table-name handling as tableConfig. */
31
+ tableName: string;
32
+ /** Omitted/empty means all eligible tables in schemaFilter. Cannot mix enabled and disabled entries. */
33
+ tables?:
34
+ SchemaConfigAuditIncludedTables<S> | SchemaConfigAuditExcludedTables<S>;
35
+ };
@@ -0,0 +1,76 @@
1
+ import { md5 } from "prostgles-types/dist/md5";
2
+ import type { Prostgles } from "../Prostgles";
3
+ import type {
4
+ TableConfig,
5
+ TableDefinition,
6
+ } from "../TableConfig/TableConfigTypes";
7
+
8
+ import { AUDIT_TABLE_COLUMN_DEFINITIONS } from "./AuditTable";
9
+ import {
10
+ AUDIT_HISTORY_PROTECTION_PREFIX,
11
+ AUDIT_TRUNCATE_PROTECTION_PREFIX,
12
+ AUDIT_WRITER_PREFIX,
13
+ } from "../TableConfig/managedTriggerNames";
14
+
15
+ export type ParsedAuditConfig = {
16
+ /** Includes previously configured history tables, whose protection is retained. */
17
+ auditTableNames: string[];
18
+ tableConfigs: TableConfig;
19
+ };
20
+
21
+ export function getAuditProtection(
22
+ tableName: string,
23
+ actions: NonNullable<TableDefinition["triggers"]>[string]["actions"],
24
+ ): NonNullable<TableDefinition["triggers"]> {
25
+ return {
26
+ [(actions.includes("update")
27
+ ? AUDIT_HISTORY_PROTECTION_PREFIX
28
+ : AUDIT_TRUNCATE_PROTECTION_PREFIX) + md5(tableName)]: {
29
+ type: "before",
30
+ actions,
31
+ forEach: "statement",
32
+ query:
33
+ "BEGIN RAISE EXCEPTION 'Audit protection: % is not allowed on %.%', TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME; END;",
34
+ },
35
+ };
36
+ }
37
+
38
+ /** Adds audit definitions to the same config used for file tables and application tables. */
39
+ export function getAuditTableConfig(
40
+ prgl: Prostgles,
41
+ tableConfig: TableConfig | undefined,
42
+ ): TableConfig | undefined {
43
+ const { audit } = prgl.opts;
44
+ let result = { ...tableConfig };
45
+ if (audit) {
46
+ const { tableName } = audit;
47
+ if (typeof tableName !== "string" || !tableName)
48
+ throw new Error("audit.tableName is required");
49
+ if (tableConfig?.[tableName]) {
50
+ throw new Error("The audit table cannot also be defined in tableConfig");
51
+ }
52
+ result = {
53
+ [tableName]: {
54
+ columns: AUDIT_TABLE_COLUMN_DEFINITIONS,
55
+ triggers: getAuditProtection(tableName, ["update", "delete", "truncate"]),
56
+ },
57
+ ...result,
58
+ };
59
+ }
60
+ for (const [name, config] of Object.entries(
61
+ prgl.parsedAuditConfig?.tableConfigs ?? {},
62
+ )) {
63
+ result[name] = {
64
+ ...result[name],
65
+ triggers: { ...result[name]?.triggers, ...config.triggers },
66
+ };
67
+ }
68
+ return Object.keys(result).length ? result : undefined;
69
+ }
70
+
71
+ export function isAuditTable(prgl: Prostgles, tableName: string): boolean {
72
+ return prgl.parsedAuditConfig?.auditTableNames.includes(tableName) ?? false;
73
+ }
74
+
75
+ export const getAuditWriterName = (auditTable: string, sourceTable: string) =>
76
+ AUDIT_WRITER_PREFIX + md5(JSON.stringify([auditTable, sourceTable]));
@@ -0,0 +1,158 @@
1
+ import { as } from "pg-promise";
2
+ import { asName } from "prostgles-types";
3
+ import type { Prostgles } from "../Prostgles";
4
+ import type { SchemaConfigAuditTableOptions } from "./AuditTypes";
5
+ import {
6
+ getAuditProtection,
7
+ getAuditWriterName,
8
+ type ParsedAuditConfig,
9
+ } from "./getAuditTableConfig";
10
+
11
+ /** Resolve column defaults against the completed schema; generate tableConfig entries only. */
12
+ export async function parseAuditConfig(
13
+ prgl: Prostgles,
14
+ ): Promise<ParsedAuditConfig> {
15
+ const { audit } = prgl.opts;
16
+ const tables = prgl.dboBuilder.tables;
17
+ const auditTableNames = [
18
+ ...new Set([
19
+ ...(prgl.parsedAuditConfig?.auditTableNames ?? []),
20
+ ...(audit ? [audit.tableName] : []),
21
+ ]),
22
+ ];
23
+ const result: ParsedAuditConfig = {
24
+ auditTableNames,
25
+ tableConfigs: {},
26
+ };
27
+ for (const name of auditTableNames) {
28
+ if (!tables.some((t) => t.name === name)) {
29
+ throw new Error(
30
+ `Audit table name must exactly match a schema table name: ${name}`,
31
+ );
32
+ }
33
+ result.tableConfigs[name] = {
34
+ triggers: getAuditProtection(name, ["update", "delete", "truncate"]),
35
+ };
36
+ }
37
+ if (!audit) return result;
38
+ const tableOptions: unknown = audit.tables;
39
+ if (
40
+ tableOptions !== undefined &&
41
+ (!tableOptions ||
42
+ typeof tableOptions !== "object" ||
43
+ Array.isArray(tableOptions))
44
+ ) {
45
+ throw new Error("audit.tables must be a table map");
46
+ }
47
+ const entries = Object.entries(audit.tables ?? {}).filter(
48
+ ([, value]) => value !== undefined,
49
+ );
50
+ const exclusion = entries.some(([, value]) => value === 0);
51
+ if (exclusion && entries.some(([, value]) => value !== 0))
52
+ throw new Error("audit.tables cannot mix enabled and disabled entries");
53
+ for (const [name, value] of entries) {
54
+ if (!tables.some((t) => t.name === name))
55
+ throw new Error(`Unknown audit table: ${name}`);
56
+ if (
57
+ value !== 0 &&
58
+ value !== 1 &&
59
+ (!value || typeof value !== "object" || Array.isArray(value))
60
+ )
61
+ throw new Error(`Invalid audit options for ${name}`);
62
+ }
63
+ const relations = await prgl.dbForSchema!.any<{
64
+ oid: number;
65
+ relkind: string;
66
+ relispartition: boolean;
67
+ }>(
68
+ "SELECT oid, relkind, relispartition FROM pg_class WHERE oid = ANY($1::oid[])",
69
+ [tables.map((t) => t.oid)],
70
+ );
71
+ for (const table of tables) {
72
+ const entry = entries.find(([name]) => name === table.name);
73
+ if (entries.length && (exclusion ? entry?.[1] === 0 : !entry)) continue;
74
+ const relation = relations.find((r) => r.oid === table.oid);
75
+ const isHistory = auditTableNames.includes(table.name);
76
+ const eligible =
77
+ !isHistory &&
78
+ table.schema !== "prostgles" &&
79
+ !table.is_view &&
80
+ !table.isHyperTable &&
81
+ relation &&
82
+ relation.relkind === "r" &&
83
+ !relation.relispartition;
84
+ if (!eligible) {
85
+ if (entry && entry[1] !== 0)
86
+ throw new Error(
87
+ `Unsupported audit target: ${table.name}. Select ordinary, non-partitioned tables.`,
88
+ );
89
+ continue;
90
+ }
91
+ const options: SchemaConfigAuditTableOptions<void, string> =
92
+ typeof entry?.[1] === "object" ? entry[1] : {};
93
+ if (
94
+ Object.keys(options).some(
95
+ (k) => !["entityType", "idColumns", "excludeColumns"].includes(k),
96
+ )
97
+ )
98
+ throw new Error(`Unknown audit option for ${table.name}`);
99
+ const ids =
100
+ options.idColumns ??
101
+ table.columns.filter((c) => c.is_pkey).map((c) => c.name);
102
+ const excluded = options.excludeColumns ?? [];
103
+ if (!Array.isArray(ids) || !ids.length)
104
+ throw new Error(
105
+ `audit.idColumns is required for ${table.name} without a primary key`,
106
+ );
107
+ for (const list of [ids, excluded]) {
108
+ if (
109
+ !Array.isArray(list) ||
110
+ new Set(list).size !== list.length ||
111
+ list.some((c) => !table.columns.some((col) => col.name === c))
112
+ )
113
+ throw new Error(`Invalid audit columns for ${table.name}`);
114
+ }
115
+ if (ids.some((c) => excluded.includes(c)))
116
+ throw new Error(
117
+ `Audit identity columns cannot be excluded: ${table.name}`,
118
+ );
119
+ if (
120
+ options.entityType !== undefined &&
121
+ (typeof options.entityType !== "string" || !options.entityType)
122
+ )
123
+ throw new Error(`Invalid audit entityType for ${table.name}`);
124
+ const functionName = getAuditWriterName(audit.tableName, table.name);
125
+ result.tableConfigs[table.name] = {
126
+ triggers: {
127
+ ...getAuditProtection(audit.tableName, ["truncate"]),
128
+ [functionName]: {
129
+ type: "after",
130
+ actions: ["insert", "update", "delete"],
131
+ forEach: "row",
132
+ query: `DECLARE
133
+ before_row jsonb; after_row jsonb; before_id jsonb; after_id jsonb;
134
+ ids text[] := ARRAY[${ids.map((c) => as.text(c)).join(", ")}]::text[];
135
+ excluded text[] := ARRAY[${excluded.map((c) => as.text(c)).join(", ")}]::text[];
136
+ BEGIN
137
+ IF TG_OP <> 'INSERT' THEN
138
+ before_row := to_jsonb(OLD);
139
+ SELECT jsonb_object_agg(k, before_row -> k) INTO before_id FROM unnest(ids) k;
140
+ before_row := before_row - excluded;
141
+ END IF;
142
+ IF TG_OP <> 'DELETE' THEN
143
+ after_row := to_jsonb(NEW);
144
+ SELECT jsonb_object_agg(k, after_row -> k) INTO after_id FROM unnest(ids) k;
145
+ after_row := after_row - excluded;
146
+ END IF;
147
+ IF TG_OP = 'UPDATE' AND before_row IS NOT DISTINCT FROM after_row THEN RETURN NULL; END IF;
148
+ INSERT INTO ${asName(audit.tableName)} (schema_name, table_name, entity_type, operation, old_id, new_id, old_row, new_row, actor)
149
+ VALUES (TG_TABLE_SCHEMA, TG_TABLE_NAME, ${as.text(options.entityType ?? table.name)}, TG_OP, before_id, after_id, before_row, after_row,
150
+ nullif(current_setting('prostgles.user', true), '')::jsonb);
151
+ RETURN NULL;
152
+ END;`,
153
+ },
154
+ },
155
+ };
156
+ }
157
+ return result;
158
+ }
@@ -250,7 +250,7 @@ export class DboBuilder {
250
250
  this.checkingCanSubscribe = false;
251
251
  }
252
252
  const start = Date.now();
253
- const tablesOrViewsReq = await getTablesForSchemaPostgresSQL(this, {
253
+ const tablesOrViewsReq = await getTablesForSchemaPostgresSQL({ db: this.prostgles.dbForSchema! }, {
254
254
  schemaFilter: this.prostgles.opts.schemaFilter,
255
255
  });
256
256
  await this.prostgles.opts.onLog?.({
@@ -262,7 +262,7 @@ export class DboBuilder {
262
262
  const tablesOrViews = tablesOrViewsReq.result;
263
263
  this.tablesOrViews = tablesOrViews;
264
264
 
265
- this.constraints = await getConstraints(this.db, this.prostgles.opts.schemaFilter);
265
+ this.constraints = await getConstraints(this.prostgles.dbForSchema!, this.prostgles.opts.schemaFilter);
266
266
  await this.prepareShortestJoinPaths();
267
267
 
268
268
  this.dbo = {};
@@ -107,6 +107,7 @@ export class QueryStreamer {
107
107
  let streamState: "started" | "ended" | "errored" | undefined;
108
108
 
109
109
  const startStream = async (client: pg.Client | undefined, query: ClientStreamedRequest) => {
110
+ await this.dboBuilder.prostgles.schemaReady;
110
111
  await this.dboBuilder.cacheDBTypes();
111
112
  const socketQuery = this.socketQueries.get(socketId)?.get(id);
112
113
  if (!socketQuery) {
@@ -25,7 +25,7 @@ export const getSchemaFilter = (schema: ProstglesInitOptions["schemaFilter"] = {
25
25
  // TODO: Add a onSocketConnect timeout for this query.
26
26
  // Reason: this query gets blocked by prostgles.app_triggers from PubSubManager.addTrigger in some cases (pg_dump locks that table)
27
27
  export async function getTablesForSchemaPostgresSQL(
28
- { db, runSQL }: Pick<DboBuilder, "db" | "runSQL">,
28
+ { db }: Pick<DboBuilder, "db">,
29
29
  {
30
30
  schemaFilter,
31
31
  ddlWithRollback,
@@ -288,7 +288,7 @@ export async function getTablesForSchemaPostgresSQL(
288
288
  });
289
289
 
290
290
  /** Get view reference cols (based on parent table) */
291
- const viewReferenceColumns = await getViewReferenceColumns(table, tableSchemaList, runSQL);
291
+ const viewReferenceColumns = await getViewReferenceColumns(table, tableSchemaList, db);
292
292
 
293
293
  table.columns = table.columns.map((col) => {
294
294
  if (col.has_default) {
@@ -1,11 +1,12 @@
1
- import { isDefined, type SQLResult } from "prostgles-types";
2
- import type { DboBuilder } from "../DboBuilder";
1
+ import { isDefined } from "prostgles-types";
2
+ import type { DB } from "../../Prostgles";
3
+ import { getDbTypes, getDetailedFieldInfo } from "../runSql/runSqlUtils";
3
4
  import type { TableSchema, TableSchemaColumn } from "../DboBuilderTypes";
4
5
 
5
6
  export const getViewReferenceColumns = async (
6
7
  table: TableSchema,
7
8
  tableSchemaList: TableSchema[],
8
- runSQL: DboBuilder["runSQL"]
9
+ db: DB
9
10
  ) => {
10
11
  const viewReferenceColumns: Pick<TableSchemaColumn, "name" | "references">[] = [];
11
12
  if (!table.is_view || !table.view_definition) {
@@ -16,12 +17,10 @@ export const getViewReferenceColumns = async (
16
17
  table.view_definition.endsWith(";") ?
17
18
  table.view_definition.slice(0, -1)
18
19
  : table.view_definition;
19
- const { fields: viewFieldsWithInfo } = (await runSQL(
20
+ const { fields } = await db.result(
20
21
  `SELECT * FROM \n ( ${view_definition} \n) t LIMIT 0`,
21
- {},
22
- {},
23
- undefined
24
- )) as SQLResult<undefined>;
22
+ );
23
+ const viewFieldsWithInfo = getDetailedFieldInfo(await getDbTypes(db), fields);
25
24
  const viewTables = tableSchemaList.filter((r) =>
26
25
  viewFieldsWithInfo.some((f) => f.tableID === r.oid)
27
26
  );
package/lib/Prostgles.ts CHANGED
@@ -63,6 +63,9 @@ import type { getAdminClient } from "./DboBuilder/runSql/getAdminClient";
63
63
  import type { TableHandler } from "./DboBuilder/TableHandler/TableHandler";
64
64
  import { getFileTableConfig } from "./StorageClient/getFileTableConfig";
65
65
  import { dirname } from "path";
66
+ import { getAuditTableConfig, type ParsedAuditConfig } from "./Audit/getAuditTableConfig";
67
+ import { syncTableTriggers } from "./TableConfig/syncTableTriggers";
68
+ import { isManagedTriggerName } from "./TableConfig/managedTriggerNames";
66
69
 
67
70
  export class Prostgles {
68
71
  /**
@@ -106,6 +109,16 @@ export class Prostgles {
106
109
 
107
110
  keywords = DEFAULT_KEYWORDS;
108
111
  loaded = false;
112
+ /** Only schema setup uses this original connection. Application handlers use db. */
113
+ dbForSchema?: DB;
114
+ schemaReady: Promise<void> = Promise.resolve();
115
+
116
+ runSchemaQueries = (queries: () => Promise<void>) => {
117
+ this.schemaReady = this.schemaReady.catch(() => {}).then(queries);
118
+ void this.schemaReady.catch(() => {});
119
+ return this.schemaReady;
120
+ };
121
+ parsedAuditConfig?: ParsedAuditConfig;
109
122
  preparingTableConfig = false;
110
123
 
111
124
  dbEventsManager?: DBEventsManager;
@@ -116,7 +129,15 @@ export class Prostgles {
116
129
  tableConfigurator?: TableConfigurator;
117
130
 
118
131
  get mergedTableConfig() {
119
- return getFileTableConfig(this);
132
+ for (const [tableName, table] of Object.entries(this.opts.tableConfig ?? {})) {
133
+ for (const name of Object.keys(table.triggers ?? {})) {
134
+ if (isManagedTriggerName(name)) {
135
+ throw new Error(`Trigger ${tableName}.${name} uses a prefix reserved for prostgles`);
136
+ }
137
+ }
138
+ }
139
+ const config = getFileTableConfig(this);
140
+ return { ...config, tableConfig: getAuditTableConfig(this, config.tableConfig) };
120
141
  }
121
142
 
122
143
  isMedia(tableName: string) {
@@ -148,6 +169,7 @@ export class Prostgles {
148
169
  onQuery: 1,
149
170
  onConnectionError: 1,
150
171
  tableConfig: 1,
172
+ audit: 1,
151
173
  tableHooks: 1,
152
174
  tableConfigMigrations: 1,
153
175
  onNotice: 1,
@@ -286,10 +308,18 @@ export class Prostgles {
286
308
 
287
309
  /** Rebuilds the DBO and replaces the application context. */
288
310
  refreshDBO = async () => {
311
+ this.loaded = false;
289
312
  await this.cleanupContext();
290
- const dbo = await this.rebuildDBO();
313
+ await this.runSchemaQueries(async () => {
314
+ await this.rebuildDBO();
315
+ if (this.opts.audit || this.parsedAuditConfig) {
316
+ await syncTableTriggers(this);
317
+ await this.rebuildDBO();
318
+ }
319
+ });
291
320
  await this.createContext({ type: "dbo.refresh" });
292
- return dbo;
321
+ this.loaded = true;
322
+ return this.dbo!;
293
323
  };
294
324
 
295
325
  initRestApi = () => {
@@ -307,7 +337,6 @@ export class Prostgles {
307
337
  if (this.tableConfigurator?.initialising) {
308
338
  console.error("TableConfigurator WILL deadlock", { reason });
309
339
  }
310
- await this.tableConfigurator?.destroy();
311
340
  this.tableConfigurator = new TableConfigurator(this);
312
341
  try {
313
342
  const now = Date.now();
@@ -323,7 +352,7 @@ export class Prostgles {
323
352
  duration: Date.now() - now,
324
353
  });
325
354
  } catch (e) {
326
- if (this.opts.tableConfigMigrations?.silentFail === false) {
355
+ if (!this.opts.audit && this.opts.tableConfigMigrations?.silentFail === false) {
327
356
  console.error("TableConfigurator silentFail: ", e);
328
357
  } else {
329
358
  throw e;
@@ -14,6 +14,7 @@ import type { StorageClient } from "./StorageClient/StorageClientTypes";
14
14
  import type { PGConstraint } from "./TableConfig/fetchTableConstraints";
15
15
  import type { TableConfig } from "./TableConfig/TableConfigTypes";
16
16
  import type { TableHooks } from "./TableHooks/TableHooks";
17
+ import type { SchemaConfigAudit } from "./Audit/AuditTypes";
17
18
 
18
19
  import type { PRGLIOSocket } from "./DboBuilder/DboBuilder";
19
20
 
@@ -312,6 +313,8 @@ export type ProstglesInitOptions<
312
313
  * made to the database schema through SQL queries
313
314
  */
314
315
  tableConfig?: TableConfig;
316
+ /** Audit committed row changes after bootstrap, before application callbacks run. */
317
+ audit?: SchemaConfigAudit<S>;
315
318
 
316
319
  /**
317
320
  * Migration logic used when the new tableConfig version is higher than the one in the database.
@@ -6,7 +6,7 @@ import type { DboBuilder } from "../DboBuilder/DboBuilder";
6
6
  export const getCreatePubSubManagerError = async (
7
7
  dboBuilder: DboBuilder
8
8
  ): Promise<string | undefined> => {
9
- const db = dboBuilder.db;
9
+ const db = dboBuilder.prostgles.dbForSchema!;
10
10
 
11
11
  const canExecute = await getCanExecute(db);
12
12
  if (!canExecute) return "Cannot run EXECUTE statements on this connection";
@@ -500,19 +500,19 @@ COMMIT;
500
500
  export const getPubSubManagerInitQuery = async function (
501
501
  this: DboBuilder,
502
502
  ): Promise<string | undefined> {
503
- const versionNum = await this.db.one("SELECT current_setting('server_version_num')::int as val");
503
+ const versionNum = await this.prostgles.dbForSchema!.one("SELECT current_setting('server_version_num')::int as val");
504
504
  const initQuery = getInitQuery(this.prostgles.opts.DEBUG_MODE, versionNum.val);
505
- const { schema_md5 = "none" } = await this.db.oneOrNone("SELECT md5($1) as schema_md5", [
505
+ const { schema_md5 = "none" } = await this.prostgles.dbForSchema!.oneOrNone("SELECT md5($1) as schema_md5", [
506
506
  initQuery.trim(),
507
507
  ]);
508
508
  const query = pgp.as.format(initQuery, { schema_md5, version });
509
- const existingSchema = await this.db.any(PROSTGLES_SCHEMA_EXISTS_QUERY);
509
+ const existingSchema = await this.prostgles.dbForSchema!.any(PROSTGLES_SCHEMA_EXISTS_QUERY);
510
510
  if (!existingSchema.length) {
511
511
  console.log("getPubSubManagerInitQuery: No prostgles.versions table found. Creating...");
512
512
  return query;
513
513
  }
514
514
  const { data: existingSchemaVersions } = await tryCatchV2(async () => {
515
- const existingSchemaVersions = await this.db.any(PROSTGLES_SCHEMA_VERSION_OK_QUERY, {
515
+ const existingSchemaVersions = await this.prostgles.dbForSchema!.any(PROSTGLES_SCHEMA_VERSION_OK_QUERY, {
516
516
  schema_md5,
517
517
  version,
518
518
  });
@@ -1,3 +1,4 @@
1
+ import { isAuditTable } from "../Audit/getAuditTableConfig";
1
2
  import { getObjectEntries, isObject } from "prostgles-types";
2
3
  import type { AuthResultWithSID } from "../Auth/AuthTypes";
3
4
  import type { TableHandler } from "../DboBuilder/TableHandler/TableHandler";
@@ -42,7 +43,7 @@ export async function getTableRulesWithoutFileTable(
42
43
 
43
44
  const isStarOrTrue = (value: any): value is "*" | true => value === "*" || value === true;
44
45
  const { privileges } = tableHandler.tableOrViewInfo;
45
- const tableRulesObject =
46
+ const rawTableRulesObject =
46
47
  isStarOrTrue(rawTableRule) ?
47
48
  {
48
49
  select: privileges.select ? TABLE_RULE_NO_LIMITS.select : undefined,
@@ -52,6 +53,10 @@ export async function getTableRulesWithoutFileTable(
52
53
  }
53
54
  : rawTableRule;
54
55
 
56
+ const tableRulesObject = isAuditTable(this.prostgles, tableName)
57
+ ? { select: rawTableRulesObject.select, insert: undefined, update: undefined, delete: undefined }
58
+ : rawTableRulesObject;
59
+
55
60
  const selectRule =
56
61
  !tableRulesObject.select ? undefined
57
62
  : isStarOrTrue(tableRulesObject.select) ? TABLE_RULE_NO_LIMITS.select
@@ -31,7 +31,7 @@ type BaseTableDefinition = {
31
31
  * Use "before" when you need to change the data before the action
32
32
  */
33
33
  type: "before" | "after" | "instead of";
34
- actions: ("insert" | "update" | "delete")[];
34
+ actions: ("insert" | "update" | "delete" | "truncate")[];
35
35
  forEach: "statement" | "row";
36
36
  /**
37
37
  * @example
@@ -23,8 +23,8 @@ export class TableConfigurator {
23
23
  return this.prostgles.dbo;
24
24
  }
25
25
  get db(): DB {
26
- if (!this.prostgles.db) throw "this.prostgles.db missing";
27
- return this.prostgles.db;
26
+ if (!this.prostgles.dbForSchema) throw "Schema database connection missing";
27
+ return this.prostgles.dbForSchema;
28
28
  }
29
29
  prostgles: Prostgles;
30
30
 
@@ -1,3 +1,4 @@
1
+ import { getManagedTriggerName } from "./managedTriggerNames";
1
2
  import { as } from "pg-promise";
2
3
  import { isDefined } from "prostgles-types";
3
4
  import type { DB } from "../initProstgles";
@@ -226,14 +227,14 @@ const getTriggerQueries = (
226
227
  ];
227
228
 
228
229
  trigger.actions.forEach((action) => {
229
- const triggerActionName = triggerFuncName + "_" + action;
230
+ const triggerActionName = getManagedTriggerName(triggerFuncName, action);
230
231
 
231
232
  const triggerActionNameParsed = asName(triggerActionName);
232
233
 
233
234
  const newTableName = action !== "delete" ? "NEW TABLE AS new_table" : "";
234
235
  const oldTableName = action !== "insert" ? "OLD TABLE AS old_table" : "";
235
236
  const transitionTables =
236
- trigger.forEach === "row" ? "" : `REFERENCING ${newTableName} ${oldTableName}`;
237
+ trigger.forEach === "row" || trigger.type !== "after" || action === "truncate" ? "" : `REFERENCING ${newTableName} ${oldTableName}`;
237
238
  queries.push(`
238
239
  CREATE TRIGGER ${triggerActionNameParsed}
239
240
  ${trigger.type} ${action} ON ${tableName}