prostgles-server 4.2.613 → 4.2.615
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/DBSchemaBuilder/DBSchemaBuilder.d.ts +2 -2
- package/dist/DBSchemaBuilder/DBSchemaBuilder.d.ts.map +1 -1
- package/dist/DboBuilder/DboBuilder.d.ts +2 -0
- package/dist/DboBuilder/DboBuilder.d.ts.map +1 -1
- package/dist/DboBuilder/DboBuilder.js +17 -3
- package/dist/DboBuilder/DboBuilder.js.map +1 -1
- package/dist/DboBuilder/TableHandler/TableHandler.d.ts +5 -5
- package/dist/DboBuilder/TableHandler/TableHandler.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/TableHandler.js +17 -1
- package/dist/DboBuilder/TableHandler/TableHandler.js.map +1 -1
- package/dist/DboBuilder/TableHandler/delete.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/delete.js +1 -0
- package/dist/DboBuilder/TableHandler/delete.js.map +1 -1
- package/dist/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.js +1 -1
- package/dist/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.js.map +1 -1
- package/dist/DboBuilder/TableHandler/insert/insert.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/insert/insert.js +22 -2
- package/dist/DboBuilder/TableHandler/insert/insert.js.map +1 -1
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts +3 -5
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js +13 -26
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js.map +1 -1
- package/dist/DboBuilder/TableHandler/prepareBeforeHookData.d.ts +4 -0
- package/dist/DboBuilder/TableHandler/prepareBeforeHookData.d.ts.map +1 -0
- package/dist/DboBuilder/TableHandler/prepareBeforeHookData.js +20 -0
- package/dist/DboBuilder/TableHandler/prepareBeforeHookData.js.map +1 -0
- package/dist/DboBuilder/TableHandler/update.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/update.js +36 -10
- package/dist/DboBuilder/TableHandler/update.js.map +1 -1
- package/dist/DboBuilder/TableHandler/updateFile.d.ts +5 -2
- package/dist/DboBuilder/TableHandler/updateFile.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/updateFile.js +17 -11
- package/dist/DboBuilder/TableHandler/updateFile.js.map +1 -1
- package/dist/DboBuilder/TableHandler/uploadFile.d.ts +3 -2
- package/dist/DboBuilder/TableHandler/uploadFile.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/uploadFile.js +15 -4
- package/dist/DboBuilder/TableHandler/uploadFile.js.map +1 -1
- package/dist/DboBuilder/runSql/getAdminClient.d.ts +3 -0
- package/dist/DboBuilder/runSql/getAdminClient.d.ts.map +1 -1
- package/dist/Logging.d.ts +1 -1
- package/dist/Logging.d.ts.map +1 -1
- package/dist/PublishParser/publishTypesAndUtils.d.ts +12 -5
- package/dist/PublishParser/publishTypesAndUtils.d.ts.map +1 -1
- package/dist/PublishParser/publishTypesAndUtils.js.map +1 -1
- package/dist/StorageClient/deleteUnreferencedFile.d.ts +4 -0
- package/dist/StorageClient/deleteUnreferencedFile.d.ts.map +1 -0
- package/dist/StorageClient/deleteUnreferencedFile.js +15 -0
- package/dist/StorageClient/deleteUnreferencedFile.js.map +1 -0
- package/dist/StorageClient/getFileStorageKey.d.ts +4 -0
- package/dist/StorageClient/getFileStorageKey.d.ts.map +1 -0
- package/dist/StorageClient/getFileStorageKey.js +7 -0
- package/dist/StorageClient/getFileStorageKey.js.map +1 -0
- package/dist/StorageClient/getFileTableConfig.d.ts +1 -0
- package/dist/StorageClient/getFileTableConfig.d.ts.map +1 -1
- package/dist/StorageClient/getFileTableConfig.js +9 -4
- package/dist/StorageClient/getFileTableConfig.js.map +1 -1
- package/dist/StorageClient/getFileType.d.ts.map +1 -1
- package/dist/StorageClient/getFileType.js +8 -10
- package/dist/StorageClient/getFileType.js.map +1 -1
- package/dist/StorageClient/getLocalStorageClient.d.ts.map +1 -1
- package/dist/StorageClient/getLocalStorageClient.js +20 -37
- package/dist/StorageClient/getLocalStorageClient.js.map +1 -1
- package/dist/StorageClient/getValidatedFileType.d.ts.map +1 -1
- package/dist/StorageClient/getValidatedFileType.js +4 -2
- package/dist/StorageClient/getValidatedFileType.js.map +1 -1
- package/dist/StorageClient/setupFileServeHandler.d.ts.map +1 -1
- package/dist/StorageClient/setupFileServeHandler.js +6 -3
- package/dist/StorageClient/setupFileServeHandler.js.map +1 -1
- package/dist/TableHooks/TableHooks.d.ts +10 -3
- package/dist/TableHooks/TableHooks.d.ts.map +1 -1
- package/lib/DBSchemaBuilder/DBSchemaBuilder.ts +3 -2
- package/lib/DboBuilder/DboBuilder.ts +18 -3
- package/lib/DboBuilder/TableHandler/TableHandler.ts +19 -1
- package/lib/DboBuilder/TableHandler/delete.ts +1 -0
- package/lib/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.ts +1 -2
- package/lib/DboBuilder/TableHandler/insert/insert.ts +31 -3
- package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +17 -33
- package/lib/DboBuilder/TableHandler/prepareBeforeHookData.ts +27 -0
- package/lib/DboBuilder/TableHandler/update.ts +40 -11
- package/lib/DboBuilder/TableHandler/updateFile.ts +25 -19
- package/lib/DboBuilder/TableHandler/uploadFile.ts +32 -7
- package/lib/Logging.ts +1 -0
- package/lib/PublishParser/publishTypesAndUtils.ts +26 -15
- package/lib/StorageClient/deleteUnreferencedFile.ts +19 -0
- package/lib/StorageClient/getFileStorageKey.ts +5 -0
- package/lib/StorageClient/getFileTableConfig.ts +19 -5
- package/lib/StorageClient/getFileType.ts +11 -14
- package/lib/StorageClient/getLocalStorageClient.ts +20 -43
- package/lib/StorageClient/getValidatedFileType.ts +4 -1
- package/lib/StorageClient/setupFileServeHandler.ts +6 -3
- package/lib/TableHooks/TableHooks.ts +23 -13
- package/package.json +8 -8
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type pgPromise from "pg-promise";
|
|
2
2
|
import type { AnyObject } from "prostgles-types";
|
|
3
|
-
import { asName
|
|
3
|
+
import { asName } from "prostgles-types";
|
|
4
4
|
import type { FileTableConfig } from "../../ProstglesTypes";
|
|
5
|
-
import type {
|
|
6
|
-
import
|
|
5
|
+
import type { OnCommit } from "../../PublishParser/publishTypesAndUtils";
|
|
6
|
+
import { deleteUnreferencedFile } from "../../StorageClient/deleteUnreferencedFile";
|
|
7
7
|
|
|
8
8
|
type OnDeleteFromFileTableArgs = {
|
|
9
9
|
returningQuery: undefined | string;
|
|
@@ -12,18 +12,14 @@ type OnDeleteFromFileTableArgs = {
|
|
|
12
12
|
filter: AnyObject;
|
|
13
13
|
};
|
|
14
14
|
t: pgPromise.ITask<{}>;
|
|
15
|
-
|
|
15
|
+
onCommit: OnCommit;
|
|
16
16
|
isOneOrNone: boolean;
|
|
17
17
|
};
|
|
18
18
|
export const onDeleteFromFileTable = async (
|
|
19
19
|
config: FileTableConfig,
|
|
20
|
-
{
|
|
20
|
+
{ onCommit, t, returningQuery, filterOpts, isOneOrNone }: OnDeleteFromFileTableArgs,
|
|
21
21
|
) => {
|
|
22
|
-
const {
|
|
23
|
-
const tableHandler = dbTX[tableName];
|
|
24
|
-
if (!tableHandler) {
|
|
25
|
-
throw new Error(`TableHandler for ${tableName} not found in dbTX`);
|
|
26
|
-
}
|
|
22
|
+
const { tableName } = config;
|
|
27
23
|
if (config.delayedDelete) {
|
|
28
24
|
const result = await t.any(
|
|
29
25
|
`UPDATE ${asName(tableName)} SET deleted = now() ${filterOpts.where} ${returningQuery ?? ""};`,
|
|
@@ -31,27 +27,15 @@ export const onDeleteFromFileTable = async (
|
|
|
31
27
|
return (isOneOrNone ? result[0] : result) as undefined | AnyObject[];
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await t.any(`DELETE FROM ${asName(tableName)} WHERE id = \${id}`, file);
|
|
46
|
-
}
|
|
47
|
-
/** If any table delete fails then do not delete files */
|
|
48
|
-
for (const file of files) {
|
|
49
|
-
await storageClient.delete(file.id);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (returningQuery) {
|
|
53
|
-
return files.map((f) => pickKeys(f, ["id"]));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return undefined;
|
|
30
|
+
const files = await t.any<Record<string, unknown> & { _prostgles_storage_key: string }>(
|
|
31
|
+
`DELETE FROM ${asName(tableName)} ${filterOpts.where}
|
|
32
|
+
${returningQuery ? `${returningQuery},` : "RETURNING"}
|
|
33
|
+
COALESCE(storage_key, id) AS _prostgles_storage_key`,
|
|
34
|
+
);
|
|
35
|
+
const result = files.map(({ _prostgles_storage_key, ...row }) => {
|
|
36
|
+
onCommit(({ db }) => deleteUnreferencedFile(db, config, _prostgles_storage_key));
|
|
37
|
+
return row;
|
|
38
|
+
});
|
|
39
|
+
if (!returningQuery) return undefined;
|
|
40
|
+
return (isOneOrNone ? result[0] : result) as AnyObject[] | undefined;
|
|
57
41
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isObject, pickKeys, type AnyObject, type FieldFilter } from "prostgles-types";
|
|
2
|
+
import { validateKeys } from "../ViewHandler/ViewHandler";
|
|
3
|
+
import type { TableHandler } from "./TableHandler";
|
|
4
|
+
|
|
5
|
+
export const prepareBeforeHookData = (
|
|
6
|
+
table: TableHandler,
|
|
7
|
+
row: AnyObject,
|
|
8
|
+
fields: FieldFilter | undefined,
|
|
9
|
+
removeDisallowedFields: boolean,
|
|
10
|
+
command: "insert" | "update",
|
|
11
|
+
) => {
|
|
12
|
+
const allowed = table.parseFieldFilter(fields, false);
|
|
13
|
+
// Nested table inserts are checked against their own publish rules later.
|
|
14
|
+
const nestedTables =
|
|
15
|
+
command === "insert" && !table.is_media ?
|
|
16
|
+
Object.keys(row).filter(
|
|
17
|
+
(key) =>
|
|
18
|
+
!table.columnSet.has(key) &&
|
|
19
|
+
table.dboBuilder.dboMap.has(key) &&
|
|
20
|
+
(isObject(row[key]) || Array.isArray(row[key]) || row[key] === undefined),
|
|
21
|
+
)
|
|
22
|
+
: [];
|
|
23
|
+
const allowedKeys = [...allowed, ...nestedTables];
|
|
24
|
+
const data = removeDisallowedFields ? pickKeys(row, allowedKeys) : { ...row };
|
|
25
|
+
validateKeys(data, allowedKeys);
|
|
26
|
+
return data;
|
|
27
|
+
};
|
|
@@ -7,7 +7,9 @@ import { prepareNewData } from "./DataValidator";
|
|
|
7
7
|
import { getInsertTableRules } from "./insert/getInsertTableRules";
|
|
8
8
|
import { getReferenceColumnInserts } from "./insert/getReferenceColumnInserts";
|
|
9
9
|
import { runInsertUpdateQuery } from "./runInsertUpdateQuery";
|
|
10
|
+
import { getFileUpdateId } from "./updateFile";
|
|
10
11
|
import type { TableHandler } from "./TableHandler";
|
|
12
|
+
import { prepareBeforeHookData } from "./prepareBeforeHookData";
|
|
11
13
|
|
|
12
14
|
export async function update(
|
|
13
15
|
this: TableHandler,
|
|
@@ -65,7 +67,37 @@ export async function update(
|
|
|
65
67
|
);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
const
|
|
70
|
+
const updateFilter = await this.prepareWhere({
|
|
71
|
+
select: undefined,
|
|
72
|
+
filter,
|
|
73
|
+
forcedFilter,
|
|
74
|
+
filterFields,
|
|
75
|
+
localParams,
|
|
76
|
+
tableRule: tableRules,
|
|
77
|
+
});
|
|
78
|
+
if (this.is_media) getFileUpdateId(filter);
|
|
79
|
+
if (this.getBeforeHooks("update", [newData]).length) {
|
|
80
|
+
newData = prepareBeforeHookData(this, newData, fields, removeDisallowedFields, "update");
|
|
81
|
+
if (params?.returnType?.startsWith("statement") || localParams?.returnQuery) {
|
|
82
|
+
throw new Error("Updates with beforeEach hooks cannot return SQL statements");
|
|
83
|
+
}
|
|
84
|
+
if (!transaction) throw new Error("beforeEach hooks require a transaction");
|
|
85
|
+
const permittedRows = await transaction.t.any(
|
|
86
|
+
withUserRLS(
|
|
87
|
+
localParams,
|
|
88
|
+
`SELECT 1 FROM ${this.escapedName} ${updateFilter.where} FOR UPDATE`,
|
|
89
|
+
true,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
if (!permittedRows.length) {
|
|
93
|
+
return params?.returning && params.multi !== false ? [] : undefined;
|
|
94
|
+
}
|
|
95
|
+
if (params?.multi === false && permittedRows.length > 1) {
|
|
96
|
+
throw `More than 1 row modified: ${permittedRows.length} rows affected`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const beforeResult = await this.beforeEach(newData, localParams, "update", updateFilter.filter);
|
|
69
101
|
newData = beforeResult.row;
|
|
70
102
|
|
|
71
103
|
const { data, allowedCols } = prepareNewData({
|
|
@@ -79,15 +111,6 @@ export async function update(
|
|
|
79
111
|
columnsAddedFromBeforeHooks: beforeResult.columnsAdded,
|
|
80
112
|
});
|
|
81
113
|
|
|
82
|
-
const updateFilter = await this.prepareWhere({
|
|
83
|
-
select: undefined,
|
|
84
|
-
filter,
|
|
85
|
-
forcedFilter,
|
|
86
|
-
filterFields,
|
|
87
|
-
localParams,
|
|
88
|
-
tableRule: tableRules,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
114
|
/**
|
|
92
115
|
* Nested inserts
|
|
93
116
|
*/
|
|
@@ -105,10 +128,13 @@ export async function update(
|
|
|
105
128
|
if (+updateCount > 1) {
|
|
106
129
|
throw "Cannot do a nestedInsert from an update that targets more than 1 row";
|
|
107
130
|
}
|
|
131
|
+
if (!+updateCount) {
|
|
132
|
+
return params?.returning && params.multi !== false ? [] : undefined;
|
|
133
|
+
}
|
|
108
134
|
if (!transaction) {
|
|
109
135
|
return wrapInTx();
|
|
110
136
|
}
|
|
111
|
-
await Promise.
|
|
137
|
+
const nestedResults = await Promise.allSettled(
|
|
112
138
|
nestedInserts.map(async (nestedInsert) => {
|
|
113
139
|
const nestedTableHandler = transaction.dbTX[nestedInsert.tableName];
|
|
114
140
|
if (!nestedTableHandler)
|
|
@@ -151,6 +177,9 @@ export async function update(
|
|
|
151
177
|
};
|
|
152
178
|
}),
|
|
153
179
|
);
|
|
180
|
+
for (const result of nestedResults) {
|
|
181
|
+
if (result.status === "rejected") throw result.reason;
|
|
182
|
+
}
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
const tx = localParams?.tx?.t || this.tx?.t || this.db;
|
|
@@ -4,18 +4,40 @@ import type { FileTableConfig } from "../../ProstglesTypes";
|
|
|
4
4
|
import type { FileTableRow } from "../../StorageClient/getFileTableConfig";
|
|
5
5
|
import type { TableHandler } from "./TableHandler";
|
|
6
6
|
import { uploadFile, type UploadFileArgs } from "./uploadFile";
|
|
7
|
+
import { deleteUnreferencedFile } from "../../StorageClient/deleteUnreferencedFile";
|
|
8
|
+
import { getFileStorageKey } from "../../StorageClient/getFileStorageKey";
|
|
7
9
|
|
|
8
10
|
type Args = Pick<
|
|
9
11
|
UploadFileArgs,
|
|
10
|
-
"localParams" | "data" | "original_name" | "original_last_modified"
|
|
12
|
+
"localParams" | "data" | "original_name" | "original_last_modified" | "onCommit" | "onRollback"
|
|
11
13
|
> & {
|
|
12
14
|
filter: AnyObject;
|
|
13
15
|
};
|
|
14
16
|
export const updateFile = async (
|
|
15
17
|
tableHandler: TableHandler,
|
|
16
18
|
config: FileTableConfig,
|
|
17
|
-
{ filter, data, original_name, original_last_modified, localParams }: Args,
|
|
19
|
+
{ filter, data, original_name, original_last_modified, localParams, onCommit, onRollback }: Args,
|
|
18
20
|
) => {
|
|
21
|
+
const existingFile = (await tableHandler.findOne(filter)) as FileTableRow | undefined;
|
|
22
|
+
|
|
23
|
+
if (!existingFile?.id) {
|
|
24
|
+
throw new Error("Existing file record not found");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onCommit(({ db }) => deleteUnreferencedFile(db, config, getFileStorageKey(existingFile)));
|
|
28
|
+
const newFile = await uploadFile(config, {
|
|
29
|
+
onCommit,
|
|
30
|
+
onRollback,
|
|
31
|
+
original_name,
|
|
32
|
+
data,
|
|
33
|
+
localParams,
|
|
34
|
+
mediaId: existingFile.id,
|
|
35
|
+
original_last_modified,
|
|
36
|
+
});
|
|
37
|
+
return { newData: omitKeys(newFile, ["id"]) };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getFileUpdateId = (filter: AnyObject): string => {
|
|
19
41
|
const { data: validFilter } = getJSONBObjectSchemaValidationError(
|
|
20
42
|
{ id: { optional: true, type: "string" } },
|
|
21
43
|
filter,
|
|
@@ -30,21 +52,5 @@ export const updateFile = async (
|
|
|
30
52
|
);
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
id: existingMediaId,
|
|
35
|
-
})) as FileTableRow | undefined;
|
|
36
|
-
|
|
37
|
-
if (!existingFile?.id) {
|
|
38
|
-
throw new Error("Existing file record not found");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await config.storageClient.delete(existingFile.id);
|
|
42
|
-
const newFile = await uploadFile(config, {
|
|
43
|
-
original_name,
|
|
44
|
-
data,
|
|
45
|
-
localParams,
|
|
46
|
-
mediaId: existingFile.id,
|
|
47
|
-
original_last_modified,
|
|
48
|
-
});
|
|
49
|
-
return { newData: omitKeys(newFile, ["id"]) };
|
|
55
|
+
return existingMediaId;
|
|
50
56
|
};
|
|
@@ -4,7 +4,12 @@ import type { FileTableConfig } from "../../ProstglesTypes";
|
|
|
4
4
|
import type { FileTableRow } from "../../StorageClient/getFileTableConfig";
|
|
5
5
|
import { getValidatedFileType } from "../../StorageClient/getValidatedFileType";
|
|
6
6
|
import { getFileServeRoute } from "../../StorageClient/setupFileServeHandler";
|
|
7
|
+
import { deleteUnreferencedFile } from "../../StorageClient/deleteUnreferencedFile";
|
|
7
8
|
import type { LocalParams } from "../DboBuilder";
|
|
9
|
+
import type {
|
|
10
|
+
OnCommitCallback,
|
|
11
|
+
TransactionCallbacks,
|
|
12
|
+
} from "../../PublishParser/publishTypesAndUtils";
|
|
8
13
|
|
|
9
14
|
const FILE_SCHEMA = {
|
|
10
15
|
id: { type: "string", optional: true },
|
|
@@ -30,7 +35,7 @@ export const assertFileObjectValid: AssertFileObjectValid = (row) => {
|
|
|
30
35
|
}
|
|
31
36
|
};
|
|
32
37
|
|
|
33
|
-
export type UploadFileArgs = {
|
|
38
|
+
export type UploadFileArgs = TransactionCallbacks & {
|
|
34
39
|
data: Buffer<ArrayBufferLike>;
|
|
35
40
|
original_name: string;
|
|
36
41
|
original_last_modified: string | null;
|
|
@@ -43,7 +48,15 @@ export type UploadFileArgs = {
|
|
|
43
48
|
|
|
44
49
|
export const uploadFile = async (
|
|
45
50
|
config: FileTableConfig,
|
|
46
|
-
{
|
|
51
|
+
{
|
|
52
|
+
data,
|
|
53
|
+
original_name,
|
|
54
|
+
original_last_modified,
|
|
55
|
+
localParams,
|
|
56
|
+
mediaId,
|
|
57
|
+
onCommit,
|
|
58
|
+
onRollback,
|
|
59
|
+
}: UploadFileArgs,
|
|
47
60
|
): Promise<Omit<FileTableRow, "added"> & Partial<Pick<FileTableRow, "added">>> => {
|
|
48
61
|
const storageClient = config.storageClient;
|
|
49
62
|
|
|
@@ -64,11 +77,22 @@ export const uploadFile = async (
|
|
|
64
77
|
content_type: type.mime,
|
|
65
78
|
};
|
|
66
79
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const storageKey = randomUUID();
|
|
81
|
+
const pendingUpload = Promise.resolve().then(async () =>
|
|
82
|
+
storageClient.upload({
|
|
83
|
+
file: data,
|
|
84
|
+
fileName: storageKey,
|
|
85
|
+
contentType: coreInfo.content_type,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
const cleanup: OnCommitCallback = async ({ db }) => {
|
|
89
|
+
// A sibling operation may have failed while this upload was still running.
|
|
90
|
+
await pendingUpload.catch(() => {});
|
|
91
|
+
await deleteUnreferencedFile(db, config, storageKey);
|
|
92
|
+
};
|
|
93
|
+
onCommit(cleanup);
|
|
94
|
+
onRollback(cleanup);
|
|
95
|
+
const uploadedInfo = await pendingUpload;
|
|
72
96
|
|
|
73
97
|
const { contentLength, contentHash } = uploadedInfo;
|
|
74
98
|
const { filePath } = uploadedInfo.type === "local" ? uploadedInfo : {};
|
|
@@ -76,6 +100,7 @@ export const uploadFile = async (
|
|
|
76
100
|
|
|
77
101
|
const fileServeRoute = getFileServeRoute(config);
|
|
78
102
|
const mediaRow: Omit<FileTableRow, "added"> = {
|
|
103
|
+
storage_key: storageKey,
|
|
79
104
|
signed_url: null,
|
|
80
105
|
signed_url_expires: null,
|
|
81
106
|
updated: new Date().toISOString(),
|
package/lib/Logging.ts
CHANGED
|
@@ -88,11 +88,12 @@ export type BeforeEachTsTrigger<R, DBX, Context = undefined> = {
|
|
|
88
88
|
*/
|
|
89
89
|
changedFields?: (keyof R)[];
|
|
90
90
|
validate: (
|
|
91
|
-
params: ValidateBeforeRowArgsCommon<R, DBX> &
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
params: ValidateBeforeRowArgsCommon<R, DBX> &
|
|
92
|
+
TransactionCallbacks<DBX> & {
|
|
93
|
+
localParams: undefined | LocalParams;
|
|
94
|
+
hookContext: AnyObject | undefined;
|
|
95
|
+
context: Context;
|
|
96
|
+
},
|
|
96
97
|
) => MaybePromise<void | { row: Partial<R>; hookContext?: AnyObject; onInserted?: () => void }>;
|
|
97
98
|
};
|
|
98
99
|
|
|
@@ -102,6 +103,16 @@ export type OnCommitCallback<DBO = DBHandlerServer> = (args: {
|
|
|
102
103
|
}) => MaybePromise<unknown>;
|
|
103
104
|
export type OnCommit<DBO = DBHandlerServer> = (callback: OnCommitCallback<DBO>) => void;
|
|
104
105
|
|
|
106
|
+
export type TransactionCallbacks<DBO = DBHandlerServer> = {
|
|
107
|
+
/** Awaited after the outer transaction commits. Errors are logged. */
|
|
108
|
+
onCommit: OnCommit<DBO>;
|
|
109
|
+
/**
|
|
110
|
+
* Awaited after the outer transaction rejects. Errors are logged.
|
|
111
|
+
* A failed COMMIT acknowledgement may have committed; verify state before destructive cleanup.
|
|
112
|
+
*/
|
|
113
|
+
onRollback: OnCommit<DBO>;
|
|
114
|
+
};
|
|
115
|
+
|
|
105
116
|
export type AfterEachTsTrigger<R, DBX, Context = undefined> = {
|
|
106
117
|
commands: Partial<Record<"insert" | "update" | "delete", 1>>;
|
|
107
118
|
/**
|
|
@@ -109,11 +120,11 @@ export type AfterEachTsTrigger<R, DBX, Context = undefined> = {
|
|
|
109
120
|
*/
|
|
110
121
|
changedFields?: (keyof R)[];
|
|
111
122
|
validate: (
|
|
112
|
-
params: ValidateRowArgsCommon<R, DBX> &
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
params: ValidateRowArgsCommon<R, DBX> &
|
|
124
|
+
TransactionCallbacks<DBX> & {
|
|
125
|
+
localParams: undefined | LocalParams;
|
|
126
|
+
context: Context;
|
|
127
|
+
},
|
|
117
128
|
) => Promise<void>;
|
|
118
129
|
};
|
|
119
130
|
|
|
@@ -152,11 +163,11 @@ export type AfterAllTsTrigger<R, DBX, Context = undefined> = {
|
|
|
152
163
|
commands: Partial<Record<"insert" | "update" | "delete", 1>>;
|
|
153
164
|
changedFields?: string[];
|
|
154
165
|
validate: (
|
|
155
|
-
params: ValidateRowsArgsCommon<R, DBX> &
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
params: ValidateRowsArgsCommon<R, DBX> &
|
|
167
|
+
TransactionCallbacks<DBX> & {
|
|
168
|
+
localParams: undefined | LocalParams;
|
|
169
|
+
context: Context;
|
|
170
|
+
},
|
|
160
171
|
) => Promise<void>;
|
|
161
172
|
};
|
|
162
173
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DB } from "../Prostgles";
|
|
2
|
+
import type { FileTableConfig } from "../ProstglesTypes";
|
|
3
|
+
|
|
4
|
+
export const deleteUnreferencedFile = async (
|
|
5
|
+
db: DB,
|
|
6
|
+
config: FileTableConfig,
|
|
7
|
+
storageKey: string,
|
|
8
|
+
) => {
|
|
9
|
+
// A failed COMMIT response does not prove rollback. Check committed state first.
|
|
10
|
+
// Fail closed if RLS would hide a reference or the database is unavailable.
|
|
11
|
+
const referenced = await db.tx(async (tx) => {
|
|
12
|
+
await tx.none("SET LOCAL row_security = off");
|
|
13
|
+
return tx.oneOrNone<{ found: number }>(
|
|
14
|
+
"SELECT 1 FROM ${table:name} WHERE storage_key = ${key}::uuid OR (storage_key IS NULL AND id = ${key}::uuid) LIMIT 1",
|
|
15
|
+
{ table: config.tableName, key: storageKey },
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
if (!referenced) await config.storageClient.delete(storageKey);
|
|
19
|
+
};
|
|
@@ -8,9 +8,11 @@ import type { TableConfig } from "../TableConfig/TableConfigTypes";
|
|
|
8
8
|
import type { TableRowFromColumnDefinitions } from "../TableConfig/TableRowFromColumnDefinitions";
|
|
9
9
|
import type { TableHooks } from "../TableHooks/TableHooks";
|
|
10
10
|
import { setupFileServeHandler } from "./setupFileServeHandler";
|
|
11
|
+
import type { TableHandlers } from "../DboBuilder/DboBuilder";
|
|
11
12
|
|
|
12
13
|
const FILE_TABLE_COLUMN_DEFINITIONS = {
|
|
13
14
|
id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,
|
|
15
|
+
storage_key: `UUID UNIQUE`, // NULL for legacy objects stored under their file ID.
|
|
14
16
|
extension: `TEXT NOT NULL DEFAULT ''`,
|
|
15
17
|
content_type: `TEXT NOT NULL DEFAULT ''`,
|
|
16
18
|
content_length: `BIGINT NOT NULL DEFAULT 0`,
|
|
@@ -101,8 +103,16 @@ export const getFileTableConfig = (
|
|
|
101
103
|
insert: 1,
|
|
102
104
|
update: 1,
|
|
103
105
|
},
|
|
104
|
-
validate: async ({
|
|
105
|
-
|
|
106
|
+
validate: async ({
|
|
107
|
+
data: insertData,
|
|
108
|
+
localParams,
|
|
109
|
+
command,
|
|
110
|
+
filter,
|
|
111
|
+
dbx,
|
|
112
|
+
onCommit,
|
|
113
|
+
onRollback,
|
|
114
|
+
}) => {
|
|
115
|
+
const tableHandler = dbx[fileTableName];
|
|
106
116
|
if (!tableHandler) throw "Storage tableHandler not found";
|
|
107
117
|
assertFileObjectValid(insertData);
|
|
108
118
|
|
|
@@ -110,6 +120,8 @@ export const getFileTableConfig = (
|
|
|
110
120
|
const data = dataBlob as unknown as Buffer;
|
|
111
121
|
if (command === "update") {
|
|
112
122
|
const { newData } = await updateFile(tableHandler, fileTable, {
|
|
123
|
+
onCommit,
|
|
124
|
+
onRollback,
|
|
113
125
|
filter: filter ?? {},
|
|
114
126
|
localParams,
|
|
115
127
|
data,
|
|
@@ -125,6 +137,8 @@ export const getFileTableConfig = (
|
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
const media = await uploadFile(fileTable, {
|
|
140
|
+
onCommit,
|
|
141
|
+
onRollback,
|
|
128
142
|
data,
|
|
129
143
|
original_name,
|
|
130
144
|
localParams,
|
|
@@ -139,12 +153,12 @@ export const getFileTableConfig = (
|
|
|
139
153
|
},
|
|
140
154
|
};
|
|
141
155
|
},
|
|
142
|
-
} satisfies BeforeEachTsTrigger<FileTableRow,
|
|
156
|
+
} satisfies BeforeEachTsTrigger<FileTableRow, TableHandlers>,
|
|
143
157
|
...(userFileTableHooks?.beforeEach || []),
|
|
144
158
|
],
|
|
145
|
-
onInsteadOfDelete: async ({
|
|
159
|
+
onInsteadOfDelete: async ({ onCommit, tx, returningQuery, isOneOrNone, filterOpts }) => {
|
|
146
160
|
return onDeleteFromFileTable(fileTable, {
|
|
147
|
-
|
|
161
|
+
onCommit,
|
|
148
162
|
t: tx,
|
|
149
163
|
returningQuery,
|
|
150
164
|
isOneOrNone,
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// const fileType = require("file-type");
|
|
2
|
-
// const res = await fileType.fromBuffer(typeof file === "string"? Buffer.from(file, 'utf8') : file);
|
|
3
|
-
|
|
4
|
-
// import * as sharp from "sharp";
|
|
5
|
-
|
|
6
1
|
import type { ALLOWED_CONTENT_TYPE, ALLOWED_EXTENSION } from "prostgles-types";
|
|
7
2
|
import { CONTENT_TYPE_TO_EXT, getKeys } from "prostgles-types";
|
|
8
3
|
|
|
@@ -17,24 +12,26 @@ export const getFileType = async (
|
|
|
17
12
|
|
|
18
13
|
const fileNameMime = getFileTypeFromFilename(fileName);
|
|
19
14
|
if (!fileNameMime?.ext) throw new Error("File name must contain extension");
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
const detectedFileType = await fileTypeFromBuffer(
|
|
16
|
+
typeof file === "string" ? Buffer.from(file, "utf8") : file,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (!detectedFileType) {
|
|
20
|
+
// Text formats may not have a detectable binary signature.
|
|
21
|
+
const { mime } = fileNameMime;
|
|
22
|
+
if (mime.startsWith("text/") || mime.endsWith("+xml") || mime === "application/sql") {
|
|
26
23
|
return fileNameMime;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
throw new Error("Could not get the file type from file buffer");
|
|
30
27
|
} else {
|
|
31
|
-
if (fileNameMime.ext.toLowerCase() !==
|
|
28
|
+
if (fileNameMime.ext.toLowerCase() !== detectedFileType.ext.toLowerCase()) {
|
|
32
29
|
throw new Error(
|
|
33
|
-
`There is a mismatch between file name extension and actual buffer extension: ${fileNameMime.ext} vs ${
|
|
30
|
+
`There is a mismatch between file name extension and actual buffer extension: ${fileNameMime.ext} vs ${detectedFileType.ext}`,
|
|
34
31
|
);
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
|
-
return
|
|
34
|
+
return detectedFileType as any;
|
|
38
35
|
};
|
|
39
36
|
|
|
40
37
|
export const getFileTypeFromFilename = (
|
|
@@ -2,6 +2,7 @@ import crypto from "crypto";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import stream from "stream";
|
|
5
|
+
import { pipeline } from "stream/promises";
|
|
5
6
|
import type {
|
|
6
7
|
UploadFileOptions,
|
|
7
8
|
LocalStorageClient,
|
|
@@ -55,52 +56,28 @@ export const getLocalStorageClient = (localConfig: LocalConfig): LocalStorageCli
|
|
|
55
56
|
const hash = crypto.createHash("md5");
|
|
56
57
|
let contentLength = 0;
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
contentLength: contentLength,
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
if (typeof args.file === "string" || Buffer.isBuffer(args.file)) {
|
|
72
|
-
const buffer = Buffer.isBuffer(args.file) ? args.file : Buffer.from(args.file);
|
|
73
|
-
|
|
74
|
-
contentLength = buffer.length;
|
|
75
|
-
hash.update(buffer);
|
|
76
|
-
|
|
77
|
-
if (args.onProgress) {
|
|
59
|
+
const buffered = typeof args.file === "string" || Buffer.isBuffer(args.file);
|
|
60
|
+
const source = buffered ? stream.Readable.from([args.file]) : args.file;
|
|
61
|
+
let lastProgress = Date.now();
|
|
62
|
+
const measure = new stream.Transform({
|
|
63
|
+
transform: (chunk: Buffer, _encoding, callback) => {
|
|
64
|
+
contentLength += chunk.length;
|
|
65
|
+
hash.update(chunk);
|
|
66
|
+
if (args.onProgress && (buffered || Date.now() - lastProgress > 1000)) {
|
|
67
|
+
lastProgress = Date.now();
|
|
78
68
|
args.onProgress(contentLength);
|
|
79
69
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
writeStream.end();
|
|
83
|
-
} else if (
|
|
84
|
-
args.file instanceof stream.Readable ||
|
|
85
|
-
(args.file as unknown) instanceof stream.PassThrough
|
|
86
|
-
) {
|
|
87
|
-
let lastProgress = Date.now();
|
|
88
|
-
const throttle = 1000; // 1 second
|
|
89
|
-
args.file.on("data", (chunk: Buffer) => {
|
|
90
|
-
contentLength += chunk.length;
|
|
91
|
-
hash.update(chunk);
|
|
92
|
-
if (args.onProgress && Date.now() - lastProgress > throttle) {
|
|
93
|
-
lastProgress = Date.now();
|
|
94
|
-
args.onProgress(contentLength);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
args.file.on("error", reject);
|
|
99
|
-
args.file.pipe(writeStream);
|
|
100
|
-
} else {
|
|
101
|
-
reject(new Error("Unsupported file type provided to upload."));
|
|
102
|
-
}
|
|
70
|
+
callback(null, chunk);
|
|
71
|
+
},
|
|
103
72
|
});
|
|
73
|
+
// Wait for all streams to close before transaction cleanup can remove a failed upload.
|
|
74
|
+
await pipeline(source, measure, fs.createWriteStream(filePath));
|
|
75
|
+
return {
|
|
76
|
+
type: "local",
|
|
77
|
+
filePath,
|
|
78
|
+
contentHash: hash.digest("hex"),
|
|
79
|
+
contentLength,
|
|
80
|
+
};
|
|
104
81
|
},
|
|
105
82
|
|
|
106
83
|
downloadAsStream: async (name: string) => {
|
|
@@ -87,6 +87,9 @@ export const getValidatedFileType = async (
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
if (!result?.mime)
|
|
90
|
+
if (!result?.mime) {
|
|
91
|
+
const extension = fileName.includes(".") ? fileName.split(".").at(-1) : "(none)";
|
|
92
|
+
throw `File MIME type not found for the provided extension: ${extension}`;
|
|
93
|
+
}
|
|
91
94
|
return result;
|
|
92
95
|
};
|