prostgles-server 4.2.606 → 4.2.608
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/Audit/AuditTable.d.ts +19 -0
- package/dist/Audit/AuditTable.d.ts.map +1 -0
- package/dist/Audit/AuditTable.js +18 -0
- package/dist/Audit/AuditTable.js.map +1 -0
- package/dist/Audit/AuditTypes.d.ts +25 -0
- package/dist/Audit/AuditTypes.d.ts.map +1 -0
- package/dist/Audit/AuditTypes.js +3 -0
- package/dist/Audit/AuditTypes.js.map +1 -0
- package/dist/Audit/getAuditTableConfig.d.ts +8 -0
- package/dist/Audit/getAuditTableConfig.d.ts.map +1 -0
- package/dist/Audit/getAuditTableConfig.js +46 -0
- package/dist/Audit/getAuditTableConfig.js.map +1 -0
- package/dist/Audit/parseAuditConfig.d.ts +7 -0
- package/dist/Audit/parseAuditConfig.d.ts.map +1 -0
- package/dist/Audit/parseAuditConfig.js +97 -0
- package/dist/Audit/parseAuditConfig.js.map +1 -0
- package/dist/DboBuilder/DboBuilder.js +2 -2
- package/dist/DboBuilder/DboBuilder.js.map +1 -1
- package/dist/DboBuilder/TableHandler/updateFile.d.ts +37 -2
- package/dist/DboBuilder/TableHandler/updateFile.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts +1 -1
- package/dist/DboBuilder/runSql/QueryStreamer.d.ts.map +1 -1
- package/dist/DboBuilder/runSql/QueryStreamer.js +1 -0
- package/dist/DboBuilder/runSql/QueryStreamer.js.map +1 -1
- package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.d.ts +3 -3
- package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.d.ts.map +1 -1
- package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.js +2 -2
- package/dist/DboBuilder/schema/getTablesForSchemaPostgresSQL.js.map +1 -1
- package/dist/DboBuilder/schema/getViewReferenceColumns.d.ts +2 -2
- package/dist/DboBuilder/schema/getViewReferenceColumns.d.ts.map +1 -1
- package/dist/DboBuilder/schema/getViewReferenceColumns.js +4 -2
- package/dist/DboBuilder/schema/getViewReferenceColumns.js.map +1 -1
- package/dist/Prostgles.d.ts +4 -0
- package/dist/Prostgles.d.ts.map +1 -1
- package/dist/Prostgles.js +32 -5
- package/dist/Prostgles.js.map +1 -1
- package/dist/ProstglesTypes.d.ts +3 -0
- package/dist/ProstglesTypes.d.ts.map +1 -1
- package/dist/ProstglesTypes.js.map +1 -1
- package/dist/PubSubManager/PubSubManager.d.ts +1 -1
- package/dist/PubSubManager/getCreatePubSubManagerError.js +1 -1
- package/dist/PubSubManager/getCreatePubSubManagerError.js.map +1 -1
- package/dist/PubSubManager/init/getPubSubManagerInitQuery.js +4 -4
- package/dist/PubSubManager/init/getPubSubManagerInitQuery.js.map +1 -1
- package/dist/PublishParser/getTableRulesWithoutFileTable.d.ts.map +1 -1
- package/dist/PublishParser/getTableRulesWithoutFileTable.js +5 -1
- package/dist/PublishParser/getTableRulesWithoutFileTable.js.map +1 -1
- package/dist/StorageClient/getFileTableConfig.d.ts +2 -4
- package/dist/StorageClient/getFileTableConfig.d.ts.map +1 -1
- package/dist/StorageClient/getFileTableConfig.js.map +1 -1
- package/dist/TableConfig/TableConfigTypes.d.ts +1 -1
- package/dist/TableConfig/TableConfigTypes.d.ts.map +1 -1
- package/dist/TableConfig/TableConfigurator.js +3 -3
- package/dist/TableConfig/TableConfigurator.js.map +1 -1
- package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts +12 -0
- package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts.map +1 -0
- package/dist/TableConfig/TableRowFromColumnDefinitions.js +3 -0
- package/dist/TableConfig/TableRowFromColumnDefinitions.js.map +1 -0
- package/dist/TableConfig/getTableConfigSchemaQueries.d.ts.map +1 -1
- package/dist/TableConfig/getTableConfigSchemaQueries.js +3 -2
- package/dist/TableConfig/getTableConfigSchemaQueries.js.map +1 -1
- package/dist/TableConfig/getTableTriggerQueries.d.ts +2 -2
- package/dist/TableConfig/getTableTriggerQueries.d.ts.map +1 -1
- package/dist/TableConfig/getTableTriggerQueries.js +55 -34
- package/dist/TableConfig/getTableTriggerQueries.js.map +1 -1
- package/dist/TableConfig/initTableConfig.d.ts.map +1 -1
- package/dist/TableConfig/initTableConfig.js +5 -6
- package/dist/TableConfig/initTableConfig.js.map +1 -1
- package/dist/TableConfig/managedTriggerNames.d.ts +8 -0
- package/dist/TableConfig/managedTriggerNames.d.ts.map +1 -0
- package/dist/TableConfig/managedTriggerNames.js +16 -0
- package/dist/TableConfig/managedTriggerNames.js.map +1 -0
- package/dist/TableConfig/runSQLFile.js +1 -1
- package/dist/TableConfig/runSQLFile.js.map +1 -1
- package/dist/TableConfig/syncTableTriggers.d.ts +4 -0
- package/dist/TableConfig/syncTableTriggers.d.ts.map +1 -0
- package/dist/TableConfig/syncTableTriggers.js +29 -0
- package/dist/TableConfig/syncTableTriggers.js.map +1 -0
- package/dist/deferDbQueries.d.ts +4 -0
- package/dist/deferDbQueries.d.ts.map +1 -0
- package/dist/deferDbQueries.js +24 -0
- package/dist/deferDbQueries.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/initProstgles.d.ts +1 -1
- package/dist/initProstgles.d.ts.map +1 -1
- package/dist/initProstgles.js +24 -13
- package/dist/initProstgles.js.map +1 -1
- package/dist/updateConfiguration.d.ts.map +1 -1
- package/dist/updateConfiguration.js +1 -6
- package/dist/updateConfiguration.js.map +1 -1
- package/lib/Audit/AuditTable.ts +20 -0
- package/lib/Audit/AuditTypes.ts +35 -0
- package/lib/Audit/getAuditTableConfig.ts +56 -0
- package/lib/Audit/parseAuditConfig.ts +101 -0
- package/lib/DboBuilder/DboBuilder.ts +2 -2
- package/lib/DboBuilder/runSql/QueryStreamer.ts +1 -0
- package/lib/DboBuilder/schema/getTablesForSchemaPostgresSQL.ts +2 -2
- package/lib/DboBuilder/schema/getViewReferenceColumns.ts +7 -8
- package/lib/Prostgles.ts +33 -5
- package/lib/ProstglesTypes.ts +3 -0
- package/lib/PubSubManager/getCreatePubSubManagerError.ts +1 -1
- package/lib/PubSubManager/init/getPubSubManagerInitQuery.ts +4 -4
- package/lib/PublishParser/getTableRulesWithoutFileTable.ts +6 -1
- package/lib/StorageClient/getFileTableConfig.ts +2 -13
- package/lib/TableConfig/TableConfigTypes.ts +1 -1
- package/lib/TableConfig/TableConfigurator.ts +2 -2
- package/lib/TableConfig/TableRowFromColumnDefinitions.ts +19 -0
- package/lib/TableConfig/getTableConfigSchemaQueries.ts +3 -2
- package/lib/TableConfig/getTableTriggerQueries.ts +82 -41
- package/lib/TableConfig/initTableConfig.ts +4 -7
- package/lib/TableConfig/managedTriggerNames.ts +15 -0
- package/lib/TableConfig/runSQLFile.ts +1 -1
- package/lib/TableConfig/syncTableTriggers.ts +31 -0
- package/lib/deferDbQueries.ts +27 -0
- package/lib/index.ts +3 -0
- package/lib/initProstgles.ts +24 -12
- package/lib/updateConfiguration.ts +1 -6
- package/package.json +1 -1
- package/dist/TableConfig/getTableTriggerQueries.spec.d.ts +0 -2
- package/dist/TableConfig/getTableTriggerQueries.spec.d.ts.map +0 -1
- package/dist/TableConfig/getTableTriggerQueries.spec.js +0 -56
- package/dist/TableConfig/getTableTriggerQueries.spec.js.map +0 -1
- package/lib/TableConfig/getTableTriggerQueries.spec.ts +0 -77
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TableRowFromColumnDefinitions } from "../TableConfig/TableRowFromColumnDefinitions";
|
|
2
|
+
|
|
3
|
+
export const AUDIT_TABLE_COLUMN_DEFINITIONS = {
|
|
4
|
+
id: "BIGSERIAL PRIMARY KEY",
|
|
5
|
+
created_at: "TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()",
|
|
6
|
+
schema_name: "TEXT NOT NULL",
|
|
7
|
+
table_name: "TEXT NOT NULL",
|
|
8
|
+
entity_type: "TEXT NOT NULL",
|
|
9
|
+
operation: { enum: ["INSERT", "UPDATE", "DELETE"] },
|
|
10
|
+
old_id: "JSONB",
|
|
11
|
+
new_id: "JSONB",
|
|
12
|
+
old_row: "JSONB",
|
|
13
|
+
new_row: "JSONB",
|
|
14
|
+
actor: "JSONB",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type AuditTableRow = TableRowFromColumnDefinitions<typeof AUDIT_TABLE_COLUMN_DEFINITIONS>;
|
|
18
|
+
export const AUDIT_TABLE_COLUMNS = Object.keys(
|
|
19
|
+
AUDIT_TABLE_COLUMN_DEFINITIONS,
|
|
20
|
+
) 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,56 @@
|
|
|
1
|
+
import { md5 } from "prostgles-types/dist/md5";
|
|
2
|
+
import type { Prostgles } from "../Prostgles";
|
|
3
|
+
import type { TableConfig, TableDefinition } from "../TableConfig/TableConfigTypes";
|
|
4
|
+
|
|
5
|
+
import { AUDIT_TABLE_COLUMN_DEFINITIONS } from "./AuditTable";
|
|
6
|
+
import {
|
|
7
|
+
AUDIT_HISTORY_PROTECTION_PREFIX,
|
|
8
|
+
AUDIT_TRUNCATE_PROTECTION_PREFIX,
|
|
9
|
+
AUDIT_WRITER_PREFIX,
|
|
10
|
+
} from "../TableConfig/managedTriggerNames";
|
|
11
|
+
|
|
12
|
+
export function getAuditProtection(
|
|
13
|
+
tableName: string,
|
|
14
|
+
actions: NonNullable<TableDefinition["triggers"]>[string]["actions"],
|
|
15
|
+
): NonNullable<TableDefinition["triggers"]> {
|
|
16
|
+
return {
|
|
17
|
+
[(actions.includes("update") ?
|
|
18
|
+
AUDIT_HISTORY_PROTECTION_PREFIX
|
|
19
|
+
: AUDIT_TRUNCATE_PROTECTION_PREFIX) + md5(tableName)]: {
|
|
20
|
+
type: "before",
|
|
21
|
+
actions,
|
|
22
|
+
forEach: "statement",
|
|
23
|
+
query:
|
|
24
|
+
"BEGIN RAISE EXCEPTION 'Audit protection: % is not allowed on %.%', TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME; END;",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Adds audit definitions to the same config used for file tables and application tables. */
|
|
30
|
+
export const getAuditTableConfig = (
|
|
31
|
+
prgl: Prostgles,
|
|
32
|
+
tableConfig: TableConfig | undefined,
|
|
33
|
+
): TableConfig | undefined => {
|
|
34
|
+
const { audit } = prgl.opts;
|
|
35
|
+
if (!audit) return tableConfig;
|
|
36
|
+
|
|
37
|
+
const { tableName } = audit;
|
|
38
|
+
if (!tableName) throw new Error("audit.tableName is required");
|
|
39
|
+
if (tableConfig?.[tableName]) {
|
|
40
|
+
throw new Error("The audit table cannot also be defined in tableConfig");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
[tableName]: {
|
|
44
|
+
columns: AUDIT_TABLE_COLUMN_DEFINITIONS,
|
|
45
|
+
triggers: getAuditProtection(tableName, ["update", "delete", "truncate"]),
|
|
46
|
+
},
|
|
47
|
+
...tableConfig,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function isAuditTable(prgl: Prostgles, tableName: string): boolean {
|
|
52
|
+
return prgl.opts.audit?.tableName === tableName;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const getAuditWriterName = (auditTable: string, sourceTable: string) =>
|
|
56
|
+
AUDIT_WRITER_PREFIX + md5(JSON.stringify([auditTable, sourceTable]));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { as } from "pg-promise";
|
|
2
|
+
import { asName, getEntries } from "prostgles-types";
|
|
3
|
+
import type { Prostgles } from "../Prostgles";
|
|
4
|
+
import type { TableConfig } from "../TableConfig/TableConfigTypes";
|
|
5
|
+
import { isArray } from "../utils/utils";
|
|
6
|
+
import { getAuditProtection, getAuditWriterName } from "./getAuditTableConfig";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve column defaults against the completed schema; generate tableConfig entries only.
|
|
10
|
+
* */
|
|
11
|
+
export const parseAuditConfig = (prgl: Prostgles): TableConfig | undefined => {
|
|
12
|
+
const { audit } = prgl.opts;
|
|
13
|
+
if (!audit) return undefined;
|
|
14
|
+
|
|
15
|
+
const tables = prgl.dboBuilder.tables;
|
|
16
|
+
if (!tables.some((t) => t.name === audit.tableName)) {
|
|
17
|
+
throw new Error(`Audit table name must exactly match a schema table name: ${audit.tableName}`);
|
|
18
|
+
}
|
|
19
|
+
const result: TableConfig = {};
|
|
20
|
+
|
|
21
|
+
const entries = getEntries(audit.tables ?? {}).filter(([, value]) => value !== undefined);
|
|
22
|
+
const exclusion = entries.some(([, value]) => value === 0);
|
|
23
|
+
if (exclusion && entries.some(([, value]) => value !== 0)) {
|
|
24
|
+
throw new Error("audit.tables cannot mix enabled and disabled entries");
|
|
25
|
+
}
|
|
26
|
+
for (const [name, value] of entries) {
|
|
27
|
+
if (!tables.some((t) => t.name === name)) throw new Error(`Unknown audit table: ${name}`);
|
|
28
|
+
if (value !== 0 && value !== 1 && (!value || typeof value !== "object" || Array.isArray(value)))
|
|
29
|
+
throw new Error(`Invalid audit options for ${name}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const table of tables) {
|
|
33
|
+
const entry = entries.find(([name]) => name === table.name);
|
|
34
|
+
if (entries.length && (exclusion ? entry?.[1] === 0 : !entry)) continue;
|
|
35
|
+
|
|
36
|
+
const isHistory = table.name === audit.tableName;
|
|
37
|
+
const eligible = !isHistory && !table.is_view && !table.isHyperTable;
|
|
38
|
+
if (!eligible) {
|
|
39
|
+
if (entry && entry[1] !== 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unsupported audit target: ${table.name}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const options = typeof entry?.[1] === "object" ? entry[1] : {};
|
|
47
|
+
const ids = options.idColumns ?? table.columns.filter((c) => c.is_pkey).map((c) => c.name);
|
|
48
|
+
const excluded = options.excludeColumns ?? [];
|
|
49
|
+
if (!isArray(ids) || !ids.length) {
|
|
50
|
+
throw new Error(`audit.idColumns is required for ${table.name} without a primary key`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const list of [ids, excluded]) {
|
|
54
|
+
if (
|
|
55
|
+
new Set(list).size !== list.length ||
|
|
56
|
+
list.some((c) => !table.columns.some((col) => col.name === c))
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(`Invalid audit columns for ${table.name}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (ids.some((c) => excluded.includes(c))) {
|
|
62
|
+
throw new Error(`Audit identity columns cannot be excluded: ${table.name}`);
|
|
63
|
+
}
|
|
64
|
+
if (options.entityType !== undefined && !options.entityType) {
|
|
65
|
+
throw new Error(`Invalid audit entityType for ${table.name}`);
|
|
66
|
+
}
|
|
67
|
+
const functionName = getAuditWriterName(audit.tableName, table.name);
|
|
68
|
+
result[table.name] = {
|
|
69
|
+
triggers: {
|
|
70
|
+
...getAuditProtection(audit.tableName, ["truncate"]),
|
|
71
|
+
[functionName]: {
|
|
72
|
+
type: "after",
|
|
73
|
+
actions: ["insert", "update", "delete"],
|
|
74
|
+
forEach: "row",
|
|
75
|
+
query: `DECLARE
|
|
76
|
+
before_row jsonb; after_row jsonb; before_id jsonb; after_id jsonb;
|
|
77
|
+
ids text[] := ARRAY[${ids.map((c) => as.text(c)).join(", ")}]::text[];
|
|
78
|
+
excluded text[] := ARRAY[${excluded.map((c) => as.text(c)).join(", ")}]::text[];
|
|
79
|
+
BEGIN
|
|
80
|
+
IF TG_OP <> 'INSERT' THEN
|
|
81
|
+
before_row := to_jsonb(OLD);
|
|
82
|
+
SELECT jsonb_object_agg(k, before_row -> k) INTO before_id FROM unnest(ids) k;
|
|
83
|
+
before_row := before_row - excluded;
|
|
84
|
+
END IF;
|
|
85
|
+
IF TG_OP <> 'DELETE' THEN
|
|
86
|
+
after_row := to_jsonb(NEW);
|
|
87
|
+
SELECT jsonb_object_agg(k, after_row -> k) INTO after_id FROM unnest(ids) k;
|
|
88
|
+
after_row := after_row - excluded;
|
|
89
|
+
END IF;
|
|
90
|
+
IF TG_OP = 'UPDATE' AND before_row IS NOT DISTINCT FROM after_row THEN RETURN NULL; END IF;
|
|
91
|
+
INSERT INTO ${asName(audit.tableName)} (schema_name, table_name, entity_type, operation, old_id, new_id, old_row, new_row, actor)
|
|
92
|
+
VALUES (TG_TABLE_SCHEMA, TG_TABLE_NAME, ${as.text(options.entityType ?? table.name)}, TG_OP, before_id, after_id, before_row, after_row,
|
|
93
|
+
nullif(current_setting('prostgles.user', true), '')::jsonb);
|
|
94
|
+
RETURN NULL;
|
|
95
|
+
END;`,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
@@ -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.
|
|
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
|
|
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,
|
|
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
|
|
2
|
-
import type {
|
|
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
|
-
|
|
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
|
|
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 } 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,15 @@ 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
|
+
};
|
|
109
121
|
preparingTableConfig = false;
|
|
110
122
|
|
|
111
123
|
dbEventsManager?: DBEventsManager;
|
|
@@ -116,7 +128,15 @@ export class Prostgles {
|
|
|
116
128
|
tableConfigurator?: TableConfigurator;
|
|
117
129
|
|
|
118
130
|
get mergedTableConfig() {
|
|
119
|
-
|
|
131
|
+
for (const [tableName, table] of Object.entries(this.opts.tableConfig ?? {})) {
|
|
132
|
+
for (const name of Object.keys(table.triggers ?? {})) {
|
|
133
|
+
if (isManagedTriggerName(name)) {
|
|
134
|
+
throw new Error(`Trigger ${tableName}.${name} uses a prefix reserved for prostgles`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const config = getFileTableConfig(this);
|
|
139
|
+
return { ...config, tableConfig: getAuditTableConfig(this, config.tableConfig) };
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
isMedia(tableName: string) {
|
|
@@ -148,6 +168,7 @@ export class Prostgles {
|
|
|
148
168
|
onQuery: 1,
|
|
149
169
|
onConnectionError: 1,
|
|
150
170
|
tableConfig: 1,
|
|
171
|
+
audit: 1,
|
|
151
172
|
tableHooks: 1,
|
|
152
173
|
tableConfigMigrations: 1,
|
|
153
174
|
onNotice: 1,
|
|
@@ -286,10 +307,18 @@ export class Prostgles {
|
|
|
286
307
|
|
|
287
308
|
/** Rebuilds the DBO and replaces the application context. */
|
|
288
309
|
refreshDBO = async () => {
|
|
310
|
+
this.loaded = false;
|
|
289
311
|
await this.cleanupContext();
|
|
290
|
-
|
|
312
|
+
await this.runSchemaQueries(async () => {
|
|
313
|
+
await this.rebuildDBO();
|
|
314
|
+
if (this.opts.audit) {
|
|
315
|
+
await syncTableTriggers(this);
|
|
316
|
+
await this.rebuildDBO();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
291
319
|
await this.createContext({ type: "dbo.refresh" });
|
|
292
|
-
|
|
320
|
+
this.loaded = true;
|
|
321
|
+
return this.dbo!;
|
|
293
322
|
};
|
|
294
323
|
|
|
295
324
|
initRestApi = () => {
|
|
@@ -307,7 +336,6 @@ export class Prostgles {
|
|
|
307
336
|
if (this.tableConfigurator?.initialising) {
|
|
308
337
|
console.error("TableConfigurator WILL deadlock", { reason });
|
|
309
338
|
}
|
|
310
|
-
await this.tableConfigurator?.destroy();
|
|
311
339
|
this.tableConfigurator = new TableConfigurator(this);
|
|
312
340
|
try {
|
|
313
341
|
const now = Date.now();
|
|
@@ -323,7 +351,7 @@ export class Prostgles {
|
|
|
323
351
|
duration: Date.now() - now,
|
|
324
352
|
});
|
|
325
353
|
} catch (e) {
|
|
326
|
-
if (this.opts.tableConfigMigrations?.silentFail === false) {
|
|
354
|
+
if (!this.opts.audit && this.opts.tableConfigMigrations?.silentFail === false) {
|
|
327
355
|
console.error("TableConfigurator silentFail: ", e);
|
|
328
356
|
} else {
|
|
329
357
|
throw e;
|
package/lib/ProstglesTypes.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
@@ -5,6 +5,7 @@ import { assertFileObjectValid, uploadFile } from "../DboBuilder/TableHandler/up
|
|
|
5
5
|
import type { Prostgles } from "../Prostgles";
|
|
6
6
|
import type { BeforeEachTsTrigger } from "../PublishParser/publishTypesAndUtils";
|
|
7
7
|
import type { TableConfig } from "../TableConfig/TableConfigTypes";
|
|
8
|
+
import type { TableRowFromColumnDefinitions } from "../TableConfig/TableRowFromColumnDefinitions";
|
|
8
9
|
import type { TableHooks } from "../TableHooks/TableHooks";
|
|
9
10
|
import { setupFileServeHandler } from "./setupFileServeHandler";
|
|
10
11
|
|
|
@@ -28,19 +29,7 @@ const FILE_TABLE_COLUMN_DEFINITIONS = {
|
|
|
28
29
|
data: `BYTEA NOT NULL CHECK (data = decode('01', 'hex'))`, // Used as a placeholder to ensure insert types are correct. Actual data is uploaded to storageClient and not stored in the DB
|
|
29
30
|
} as const;
|
|
30
31
|
|
|
31
|
-
type
|
|
32
|
-
|
|
33
|
-
export type FileTableRow = {
|
|
34
|
-
[K in keyof FileTableColumnDefinitions as K]: FileTableColumnDefinitions[K] extends (
|
|
35
|
-
`BYTEA NOT NULL${string}`
|
|
36
|
-
) ?
|
|
37
|
-
Buffer
|
|
38
|
-
: FileTableColumnDefinitions[K] extends (
|
|
39
|
-
`${string} NOT NULL${string}` | `${string}PRIMARY KEY${string}`
|
|
40
|
-
) ?
|
|
41
|
-
string
|
|
42
|
-
: string | null;
|
|
43
|
-
};
|
|
32
|
+
export type FileTableRow = TableRowFromColumnDefinitions<typeof FILE_TABLE_COLUMN_DEFINITIONS>;
|
|
44
33
|
|
|
45
34
|
export const getFileTableConfig = (
|
|
46
35
|
prg: Prostgles,
|
|
@@ -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.
|
|
27
|
-
return this.prostgles.
|
|
26
|
+
if (!this.prostgles.dbForSchema) throw "Schema database connection missing";
|
|
27
|
+
return this.prostgles.dbForSchema;
|
|
28
28
|
}
|
|
29
29
|
prostgles: Prostgles;
|
|
30
30
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Row types for the column definitions used by managed tables. */
|
|
2
|
+
export type TableRowFromColumnDefinitions<
|
|
3
|
+
T extends Record<
|
|
4
|
+
string,
|
|
5
|
+
string | { enum: readonly (string | number)[]; nullable?: boolean }
|
|
6
|
+
>,
|
|
7
|
+
> = {
|
|
8
|
+
[K in keyof T]: T[K] extends { enum: readonly (infer V)[] }
|
|
9
|
+
? V | (T[K] extends { nullable: true } ? null : never)
|
|
10
|
+
: | (T[K] extends `BYTEA${string}`
|
|
11
|
+
? Buffer
|
|
12
|
+
: T[K] extends `JSONB${string}`
|
|
13
|
+
? Record<string, unknown>
|
|
14
|
+
: string)
|
|
15
|
+
| (T[K] extends
|
|
16
|
+
`${string} NOT NULL${string}` | `${string}PRIMARY KEY${string}`
|
|
17
|
+
? never
|
|
18
|
+
: null);
|
|
19
|
+
};
|
|
@@ -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
|
|
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}
|