prostgles-server 4.2.625 → 4.2.626
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 +2 -1
- package/dist/Audit/AuditTable.d.ts.map +1 -1
- package/dist/Audit/AuditTable.js.map +1 -1
- package/dist/Audit/getAuditTableRules.d.ts.map +1 -1
- package/dist/Audit/getAuditTableRules.js +3 -3
- package/dist/Audit/getAuditTableRules.js.map +1 -1
- package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.d.ts.map +1 -1
- package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.js +7 -1
- package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.js.map +1 -1
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts +3 -1
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js +19 -5
- package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js.map +1 -1
- package/dist/DboBuilder/TableHandler/updateFile.d.ts +2 -39
- package/dist/DboBuilder/TableHandler/updateFile.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/updateFile.js +1 -0
- package/dist/DboBuilder/TableHandler/updateFile.js.map +1 -1
- package/dist/DboBuilder/TableHandler/uploadFile.d.ts +2 -1
- package/dist/DboBuilder/TableHandler/uploadFile.d.ts.map +1 -1
- package/dist/DboBuilder/TableHandler/uploadFile.js +2 -1
- package/dist/DboBuilder/TableHandler/uploadFile.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/getRawInfo.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/getRawInfo.js +6 -2
- package/dist/DboBuilder/ViewHandler/getRawInfo.js.map +1 -1
- package/dist/DboBuilder/getColumns.d.ts.map +1 -1
- package/dist/DboBuilder/getColumns.js +3 -0
- package/dist/DboBuilder/getColumns.js.map +1 -1
- package/dist/ProstglesTypes.d.ts +10 -0
- package/dist/ProstglesTypes.d.ts.map +1 -1
- package/dist/ProstglesTypes.js.map +1 -1
- package/dist/PublishParser/PublishParser.d.ts.map +1 -1
- package/dist/PublishParser/PublishParser.js +7 -0
- package/dist/PublishParser/PublishParser.js.map +1 -1
- package/dist/PublishParser/getFileTableRules.d.ts.map +1 -1
- package/dist/PublishParser/getFileTableRules.js +4 -1
- package/dist/PublishParser/getFileTableRules.js.map +1 -1
- package/dist/PublishParser/getFileVersionTableRules.d.ts +7 -0
- package/dist/PublishParser/getFileVersionTableRules.d.ts.map +1 -0
- package/dist/PublishParser/getFileVersionTableRules.js +77 -0
- package/dist/PublishParser/getFileVersionTableRules.js.map +1 -0
- package/dist/PublishParser/getFileVersionTableRules.spec.d.ts +2 -0
- package/dist/PublishParser/getFileVersionTableRules.spec.d.ts.map +1 -0
- package/dist/PublishParser/getFileVersionTableRules.spec.js +50 -0
- package/dist/PublishParser/getFileVersionTableRules.spec.js.map +1 -0
- package/dist/PublishParser/getPublishedTableNames.d.ts.map +1 -1
- package/dist/PublishParser/getPublishedTableNames.js +11 -4
- package/dist/PublishParser/getPublishedTableNames.js.map +1 -1
- package/dist/StorageClient/deleteUnreferencedFile.d.ts.map +1 -1
- package/dist/StorageClient/deleteUnreferencedFile.js +17 -1
- package/dist/StorageClient/deleteUnreferencedFile.js.map +1 -1
- package/dist/StorageClient/fileTableDefinitions.d.ts +54 -0
- package/dist/StorageClient/fileTableDefinitions.d.ts.map +1 -0
- package/dist/StorageClient/fileTableDefinitions.js +38 -0
- package/dist/StorageClient/fileTableDefinitions.js.map +1 -0
- package/dist/StorageClient/fileVersionUtils.d.ts +18 -0
- package/dist/StorageClient/fileVersionUtils.d.ts.map +1 -0
- package/dist/StorageClient/fileVersionUtils.js +22 -0
- package/dist/StorageClient/fileVersionUtils.js.map +1 -0
- package/dist/StorageClient/getFileStorageKey.d.ts +3 -1
- package/dist/StorageClient/getFileStorageKey.d.ts.map +1 -1
- package/dist/StorageClient/getFileStorageKey.js +9 -1
- package/dist/StorageClient/getFileStorageKey.js.map +1 -1
- package/dist/StorageClient/getFileTableConfig.d.ts +1 -23
- package/dist/StorageClient/getFileTableConfig.d.ts.map +1 -1
- package/dist/StorageClient/getFileTableConfig.js +63 -25
- package/dist/StorageClient/getFileTableConfig.js.map +1 -1
- package/dist/StorageClient/getFileTableConfig.spec.js +6 -1
- package/dist/StorageClient/getFileTableConfig.spec.js.map +1 -1
- package/dist/StorageClient/saveFileVersion.d.ts +7 -0
- package/dist/StorageClient/saveFileVersion.d.ts.map +1 -0
- package/dist/StorageClient/saveFileVersion.js +71 -0
- package/dist/StorageClient/saveFileVersion.js.map +1 -0
- package/dist/StorageClient/setupFileServeHandler.d.ts.map +1 -1
- package/dist/StorageClient/setupFileServeHandler.js +38 -21
- package/dist/StorageClient/setupFileServeHandler.js.map +1 -1
- package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts +11 -4
- package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts.map +1 -1
- package/dist/TableConfig/initTableConfig.d.ts.map +1 -1
- package/dist/TableConfig/initTableConfig.js +9 -6
- package/dist/TableConfig/initTableConfig.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/lib/Audit/AuditTable.ts +4 -1
- package/lib/Audit/getAuditTableRules.ts +5 -1
- package/lib/DBSchemaBuilder/getClientDBGeneratedSchemas.ts +9 -1
- package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +31 -5
- package/lib/DboBuilder/TableHandler/updateFile.ts +1 -0
- package/lib/DboBuilder/TableHandler/uploadFile.ts +3 -0
- package/lib/DboBuilder/ViewHandler/getRawInfo.ts +6 -2
- package/lib/DboBuilder/getColumns.ts +2 -0
- package/lib/ProstglesTypes.ts +11 -0
- package/lib/PublishParser/PublishParser.ts +25 -7
- package/lib/PublishParser/getFileTableRules.ts +7 -1
- package/lib/PublishParser/getFileVersionTableRules.spec.ts +57 -0
- package/lib/PublishParser/getFileVersionTableRules.ts +106 -0
- package/lib/PublishParser/getPublishedTableNames.ts +14 -5
- package/lib/StorageClient/deleteUnreferencedFile.ts +20 -2
- package/lib/StorageClient/fileTableDefinitions.ts +52 -0
- package/lib/StorageClient/fileVersionUtils.ts +37 -0
- package/lib/StorageClient/getFileStorageKey.ts +11 -2
- package/lib/StorageClient/getFileTableConfig.spec.ts +6 -1
- package/lib/StorageClient/getFileTableConfig.ts +85 -30
- package/lib/StorageClient/saveFileVersion.ts +82 -0
- package/lib/StorageClient/setupFileServeHandler.ts +51 -28
- package/lib/TableConfig/TableRowFromColumnDefinitions.ts +37 -13
- package/lib/TableConfig/initTableConfig.ts +10 -6
- package/lib/index.ts +2 -1
- package/package.json +1 -1
|
@@ -86,6 +86,9 @@ export const getClientDBGeneratedSchemas = (
|
|
|
86
86
|
` ${JSON.stringify(table.name)}: ${tableType} & { insertColumns: ${input}; updateColumns: ${updateInput};${optionalTable} };`,
|
|
87
87
|
];
|
|
88
88
|
});
|
|
89
|
+
if (!definitions.length) {
|
|
90
|
+
return `export type ${name} = Record<string, never>;`;
|
|
91
|
+
}
|
|
89
92
|
return `export type ${name} = {\n${definitions.join("\n")}\n};`;
|
|
90
93
|
};
|
|
91
94
|
|
|
@@ -105,9 +108,14 @@ const reservedNames = new Set(Object.values(DB_GENERATED_NAMES) as string[]);
|
|
|
105
108
|
|
|
106
109
|
export const validateClientSchemaName = (name: string) => {
|
|
107
110
|
// Requiring a Schema suffix also avoids TypeScript keywords and primitive type names.
|
|
108
|
-
if (!/^[A-Za-z_$][\w$]*Schema$/.test(name)
|
|
111
|
+
if (!/^[A-Za-z_$][\w$]*Schema$/.test(name)) {
|
|
109
112
|
throw new Error(
|
|
110
113
|
`Invalid client schema name: ${JSON.stringify(name)}. Use a unique name ending in Schema.`,
|
|
111
114
|
);
|
|
112
115
|
}
|
|
116
|
+
if (reservedNames.has(name)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Invalid client schema name: ${JSON.stringify(name)}. Use a unique name ending in Schema that does not conflict with reserved names (${Array.from(reservedNames).join(", ")}).`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
113
121
|
};
|
|
@@ -4,6 +4,11 @@ import { asName } from "prostgles-types";
|
|
|
4
4
|
import type { FileTableConfig } from "../../ProstglesTypes";
|
|
5
5
|
import type { OnCommit } from "../../PublishParser/publishTypesAndUtils";
|
|
6
6
|
import { deleteUnreferencedFile } from "../../StorageClient/deleteUnreferencedFile";
|
|
7
|
+
import {
|
|
8
|
+
getFileRevisionsTableHandler,
|
|
9
|
+
getFileTableHandler,
|
|
10
|
+
} from "../../StorageClient/fileVersionUtils";
|
|
11
|
+
import type { TableHandlers } from "../DboBuilder";
|
|
7
12
|
|
|
8
13
|
type OnDeleteFromFileTableArgs = {
|
|
9
14
|
returningQuery: undefined | string;
|
|
@@ -12,12 +17,14 @@ type OnDeleteFromFileTableArgs = {
|
|
|
12
17
|
filter: AnyObject;
|
|
13
18
|
};
|
|
14
19
|
t: pgPromise.ITask<{}>;
|
|
20
|
+
dbx: TableHandlers;
|
|
15
21
|
onCommit: OnCommit;
|
|
16
22
|
isOneOrNone: boolean;
|
|
17
23
|
};
|
|
24
|
+
|
|
18
25
|
export const onDeleteFromFileTable = async (
|
|
19
26
|
config: FileTableConfig,
|
|
20
|
-
{ onCommit, t, returningQuery, filterOpts, isOneOrNone }: OnDeleteFromFileTableArgs,
|
|
27
|
+
{ onCommit, t, dbx, returningQuery, filterOpts, isOneOrNone }: OnDeleteFromFileTableArgs,
|
|
21
28
|
) => {
|
|
22
29
|
const { tableName } = config;
|
|
23
30
|
if (config.delayedDelete) {
|
|
@@ -27,15 +34,34 @@ export const onDeleteFromFileTable = async (
|
|
|
27
34
|
return (isOneOrNone ? result[0] : result) as undefined | AnyObject[];
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
const fileIds =
|
|
38
|
+
config.versioning ?
|
|
39
|
+
((await getFileTableHandler(dbx, config).find(filterOpts.filter, {
|
|
40
|
+
select: { id: 1 },
|
|
41
|
+
limit: null,
|
|
42
|
+
})) as { id: string }[])
|
|
43
|
+
: [];
|
|
44
|
+
const versionFiles =
|
|
45
|
+
fileIds.length ?
|
|
46
|
+
((await getFileRevisionsTableHandler(dbx, config).find(
|
|
47
|
+
{ file_id: { $in: fileIds.map(({ id }) => id) } },
|
|
48
|
+
{ select: { storage_key: 1 }, limit: null },
|
|
49
|
+
)) as { storage_key: string }[])
|
|
50
|
+
: [];
|
|
51
|
+
|
|
30
52
|
const files = await t.any<Record<string, unknown> & { _prostgles_storage_key: string }>(
|
|
31
53
|
`DELETE FROM ${asName(tableName)} ${filterOpts.where}
|
|
32
54
|
${returningQuery ? `${returningQuery},` : "RETURNING"}
|
|
33
55
|
COALESCE(storage_key, id) AS _prostgles_storage_key`,
|
|
34
56
|
);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
const storageKeys = new Set([
|
|
58
|
+
...versionFiles.map(({ storage_key }) => storage_key),
|
|
59
|
+
...files.map(({ _prostgles_storage_key }) => _prostgles_storage_key),
|
|
60
|
+
]);
|
|
61
|
+
for (const storageKey of storageKeys) {
|
|
62
|
+
onCommit(({ db }) => deleteUnreferencedFile(db, config, storageKey));
|
|
63
|
+
}
|
|
64
|
+
const result = files.map(({ _prostgles_storage_key, ...row }) => row);
|
|
39
65
|
if (!returningQuery) return undefined;
|
|
40
66
|
return (isOneOrNone ? result[0] : result) as AnyObject[] | undefined;
|
|
41
67
|
};
|
|
@@ -44,6 +44,7 @@ export type UploadFileArgs = TransactionCallbacks & {
|
|
|
44
44
|
* Used to update an existing file
|
|
45
45
|
*/
|
|
46
46
|
mediaId: string | undefined;
|
|
47
|
+
version?: number;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
export const uploadFile = async (
|
|
@@ -54,6 +55,7 @@ export const uploadFile = async (
|
|
|
54
55
|
original_last_modified,
|
|
55
56
|
localParams,
|
|
56
57
|
mediaId,
|
|
58
|
+
version,
|
|
57
59
|
onCommit,
|
|
58
60
|
onRollback,
|
|
59
61
|
}: UploadFileArgs,
|
|
@@ -115,6 +117,7 @@ export const uploadFile = async (
|
|
|
115
117
|
url: [fileServeRoute, coreInfo.id].join("/"),
|
|
116
118
|
content_length: String(contentLength),
|
|
117
119
|
data: Buffer.from([0x01]),
|
|
120
|
+
...(config.versioning ? { version: version ?? 1 } : {}),
|
|
118
121
|
};
|
|
119
122
|
|
|
120
123
|
const isInsert = !mediaId;
|
|
@@ -2,6 +2,7 @@ import { type TableInfo as TInfo } from "prostgles-types/dist";
|
|
|
2
2
|
import type { ParsedTableRule } from "../../PublishParser/PublishParser";
|
|
3
3
|
import type { LocalParams } from "../DboBuilder";
|
|
4
4
|
import type { ViewHandler } from "./ViewHandler";
|
|
5
|
+
import { isFileVersionTable } from "../../StorageClient/fileVersionUtils";
|
|
5
6
|
|
|
6
7
|
/** Table metadata before client schema customisation. */
|
|
7
8
|
export function getRawInfo(
|
|
@@ -11,7 +12,9 @@ export function getRawInfo(
|
|
|
11
12
|
localParams?: LocalParams,
|
|
12
13
|
): TInfo {
|
|
13
14
|
const validatedTableRules = this.getValidatedRules(tableRules, localParams);
|
|
14
|
-
const
|
|
15
|
+
const fileTableConfig = this.dboBuilder.prostgles.opts.fileTable;
|
|
16
|
+
const fileTableName = fileTableConfig?.tableName;
|
|
17
|
+
const isVersionTable = isFileVersionTable(fileTableConfig, this.name);
|
|
15
18
|
const allowedFieldsToSelect = this.parseFieldFilter(tableRules?.select?.fields);
|
|
16
19
|
const { requiredNestedInserts, allowedNestedInserts } = tableRules?.insert ?? {};
|
|
17
20
|
const label = this.dboBuilder.prostgles.tableConfigurator?.getTableLabel({
|
|
@@ -32,10 +35,11 @@ export function getRawInfo(
|
|
|
32
35
|
isView: this.isView,
|
|
33
36
|
hasFiles: Boolean(
|
|
34
37
|
!this.is_media &&
|
|
38
|
+
!isVersionTable &&
|
|
35
39
|
fileTableName &&
|
|
36
40
|
this.columns.some((c) => c.references?.some((r) => r.ftable === fileTableName)),
|
|
37
41
|
),
|
|
38
|
-
fileTableName,
|
|
42
|
+
fileTableName: isVersionTable ? undefined : fileTableName,
|
|
39
43
|
dynamicRules: {
|
|
40
44
|
update: Boolean(tableRules?.update?.dynamicFields?.length),
|
|
41
45
|
},
|
|
@@ -6,6 +6,7 @@ import { getErrorAsObject, getSerializedClientErrorFromPGError } from "./DboBuil
|
|
|
6
6
|
import type { TableHandler } from "./TableHandler/TableHandler";
|
|
7
7
|
import type { ViewHandler } from "./ViewHandler/ViewHandler";
|
|
8
8
|
import { getRawInfo } from "./ViewHandler/getRawInfo";
|
|
9
|
+
import { isFileVersionTable } from "../StorageClient/fileVersionUtils";
|
|
9
10
|
|
|
10
11
|
export const isTableHandler = (v: any): v is TableHandler => "parseUpdateRules" in v;
|
|
11
12
|
|
|
@@ -173,6 +174,7 @@ const getFileColumnInfo = (args: {
|
|
|
173
174
|
if (!fileTableConfig) return undefined;
|
|
174
175
|
const { colName, tableHandler } = args;
|
|
175
176
|
const tableName = tableHandler.name;
|
|
177
|
+
if (isFileVersionTable(fileTableConfig, tableName)) return undefined;
|
|
176
178
|
const tableConfig = fileTableConfig.referencedTables?.[tableName];
|
|
177
179
|
const isReferencingFileTable = tableHandler.columns.some(
|
|
178
180
|
(c) =>
|
package/lib/ProstglesTypes.ts
CHANGED
|
@@ -70,6 +70,17 @@ export type FileTableConfig = {
|
|
|
70
70
|
checkIntervalHours?: number;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Keeps immutable metadata records for every uploaded file revision.
|
|
75
|
+
* Existing file IDs and URLs continue to resolve to the latest version.
|
|
76
|
+
*/
|
|
77
|
+
versioning?: {
|
|
78
|
+
/** Defaults to `${tableName}_versions`. */
|
|
79
|
+
tableName?: string;
|
|
80
|
+
/** Maximum versions kept per file. Undefined keeps every version. */
|
|
81
|
+
maxVersions?: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
73
84
|
/**
|
|
74
85
|
* Specifying referencedTables with referenceColumns allows restricting the
|
|
75
86
|
* allowed file types that can be inserted and referenced in the specified tables.
|
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
UnrestrictedFunctionContext,
|
|
18
18
|
} from "./defineServerFunction";
|
|
19
19
|
import { getFileTableRules } from "./getFileTableRules";
|
|
20
|
+
import { getFileVersionTableRules } from "./getFileVersionTableRules";
|
|
21
|
+
import { isFileVersionTable } from "../StorageClient/fileVersionUtils";
|
|
20
22
|
import { getPublishedObjectFromResult } from "./getPublishedObjectFromResult";
|
|
21
23
|
import { getSchemaFromPublish } from "./getSchemaFromPublish";
|
|
22
24
|
import { getParsedPublishTable } from "./getParsedPublishTable";
|
|
@@ -313,13 +315,12 @@ export class PublishParser {
|
|
|
313
315
|
(isAuditTable && clientReq ?
|
|
314
316
|
await this.getPublishObjectForUser(clientReq, clientInfo)
|
|
315
317
|
: undefined);
|
|
316
|
-
const publishRulesExcludingInferredTables =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
318
|
+
const publishRulesExcludingInferredTables = await this.getParsedPublishTable({
|
|
319
|
+
clientReq,
|
|
320
|
+
tableName,
|
|
321
|
+
clientInfo,
|
|
322
|
+
resolvedPublishObject,
|
|
323
|
+
});
|
|
323
324
|
if (audit && isAuditTable) {
|
|
324
325
|
const auditTableRules = await getAuditTableRules.bind(this)(
|
|
325
326
|
audit,
|
|
@@ -336,6 +337,23 @@ export class PublishParser {
|
|
|
336
337
|
scope,
|
|
337
338
|
);
|
|
338
339
|
}
|
|
340
|
+
const fileConfig = this.prostgles.opts.fileTable;
|
|
341
|
+
if (isFileVersionTable(fileConfig, tableName)) {
|
|
342
|
+
const versionRules = await getFileVersionTableRules.bind(this)(
|
|
343
|
+
fileConfig,
|
|
344
|
+
publishRulesExcludingInferredTables,
|
|
345
|
+
clientReq,
|
|
346
|
+
clientInfo,
|
|
347
|
+
scope,
|
|
348
|
+
overriddenPublish,
|
|
349
|
+
);
|
|
350
|
+
return applyScopeToTableRules(
|
|
351
|
+
tableName,
|
|
352
|
+
tableHandler,
|
|
353
|
+
parsePublishTableRule(versionRules),
|
|
354
|
+
scope,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
339
357
|
if (this.dbo[tableName]?.is_media) {
|
|
340
358
|
const { rules: fileTableRules } = await getFileTableRules.bind(this)(
|
|
341
359
|
tableName,
|
|
@@ -5,6 +5,7 @@ import { parseFieldFilter } from "../DboBuilder/ViewHandler/parseFieldFilter";
|
|
|
5
5
|
import type { PublishParser } from "./PublishParser";
|
|
6
6
|
import type { ParsedPublishTable, PublishObject, UpdateRule } from "./publishTypesAndUtils";
|
|
7
7
|
import type { LocalParams } from "../DboBuilder/DboBuilder";
|
|
8
|
+
import { isFileVersionTable } from "../StorageClient/fileVersionUtils";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Permissions for referencedTables columns are propagated to the file table (even if file table has no permissions)
|
|
@@ -28,7 +29,12 @@ export async function getFileTableRules(
|
|
|
28
29
|
const forcedUpdateFilters: FullFilter<AnyObject, void>[] = [];
|
|
29
30
|
const allowedNestedInserts: { table: string; column: string }[] = [];
|
|
30
31
|
const referencedColumns = this.prostgles.dboBuilder.tablesOrViews
|
|
31
|
-
?.filter(
|
|
32
|
+
?.filter(
|
|
33
|
+
(t) =>
|
|
34
|
+
!t.is_view &&
|
|
35
|
+
t.name !== fileTableName &&
|
|
36
|
+
!isFileVersionTable(this.prostgles.opts.fileTable, t.name),
|
|
37
|
+
)
|
|
32
38
|
.map((t) => {
|
|
33
39
|
const refCols = t.columns.filter((c) =>
|
|
34
40
|
c.references?.some((r) => r.ftable === fileTableName),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { FileTableConfig } from "../ProstglesTypes";
|
|
4
|
+
import { parseFieldFilter } from "../DboBuilder/ViewHandler/parseFieldFilter";
|
|
5
|
+
import type { PublishParser } from "./PublishParser";
|
|
6
|
+
import { getFileVersionTableRules } from "./getFileVersionTableRules";
|
|
7
|
+
|
|
8
|
+
void test("file version rules inherit fields and restrictive select options", async () => {
|
|
9
|
+
const fileColumns = ["id", "updated", "metadata"];
|
|
10
|
+
const versionColumns = ["id", "file_id", "created", "metadata"];
|
|
11
|
+
const getHandler = (columns: string[]) => ({
|
|
12
|
+
column_names: columns,
|
|
13
|
+
parseFieldFilter: (fields: any = "*") => parseFieldFilter(fields, true, columns),
|
|
14
|
+
});
|
|
15
|
+
const parser = {
|
|
16
|
+
dbo: {
|
|
17
|
+
files: getHandler(fileColumns),
|
|
18
|
+
files_versions: getHandler(versionColumns),
|
|
19
|
+
},
|
|
20
|
+
getTableRules: () => Promise.resolve({
|
|
21
|
+
select: {
|
|
22
|
+
fields: "*",
|
|
23
|
+
maxLimit: 5,
|
|
24
|
+
subscribeThrottle: 20,
|
|
25
|
+
disableMethods: { subscribe: 1 },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
} as unknown as PublishParser;
|
|
29
|
+
|
|
30
|
+
const rules = await getFileVersionTableRules.call(
|
|
31
|
+
parser,
|
|
32
|
+
{ tableName: "files", versioning: {} } as FileTableConfig,
|
|
33
|
+
{
|
|
34
|
+
select: {
|
|
35
|
+
fields: "*",
|
|
36
|
+
maxLimit: 10,
|
|
37
|
+
subscribeThrottle: 10,
|
|
38
|
+
disableMethods: { sync: 1 },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
undefined,
|
|
42
|
+
undefined,
|
|
43
|
+
undefined,
|
|
44
|
+
undefined,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
assert(rules?.select);
|
|
48
|
+
assert.deepEqual(rules.select.fields, {
|
|
49
|
+
id: 1,
|
|
50
|
+
file_id: 1,
|
|
51
|
+
created: 1,
|
|
52
|
+
metadata: 1,
|
|
53
|
+
});
|
|
54
|
+
assert.equal(rules.select.maxLimit, 5);
|
|
55
|
+
assert.equal(rules.select.subscribeThrottle, 20);
|
|
56
|
+
assert.deepEqual(rules.select.disableMethods, { subscribe: 1, sync: 1 });
|
|
57
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { fromEntries } from "prostgles-types";
|
|
2
|
+
import type { AuthClientRequest, AuthResultWithSID } from "../Auth/AuthTypes";
|
|
3
|
+
import type { LocalParams } from "../DboBuilder/DboBuilder";
|
|
4
|
+
import type { FileTableConfig } from "../ProstglesTypes";
|
|
5
|
+
import type { PublishParser } from "./PublishParser";
|
|
6
|
+
import type { ParsedPublishTable, PublishObject } from "./publishTypesAndUtils";
|
|
7
|
+
import { getFileVersionTableName } from "../StorageClient/fileVersionUtils";
|
|
8
|
+
import type { FileTableRow, FileVersionTableRow } from "../StorageClient/fileTableDefinitions";
|
|
9
|
+
|
|
10
|
+
export const getFileVersionTableRules = async function (
|
|
11
|
+
this: PublishParser,
|
|
12
|
+
config: FileTableConfig,
|
|
13
|
+
explicitRules: ParsedPublishTable | undefined,
|
|
14
|
+
clientReq: AuthClientRequest | undefined,
|
|
15
|
+
clientInfo: AuthResultWithSID | undefined,
|
|
16
|
+
scope: LocalParams["scope"] | undefined,
|
|
17
|
+
resolvedPublishObject: PublishObject | undefined,
|
|
18
|
+
): Promise<ParsedPublishTable | undefined> {
|
|
19
|
+
const versionTableName = getFileVersionTableName(config);
|
|
20
|
+
if (
|
|
21
|
+
resolvedPublishObject &&
|
|
22
|
+
Object.hasOwn(resolvedPublishObject, versionTableName) &&
|
|
23
|
+
!explicitRules?.select
|
|
24
|
+
) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fileRules = await this.getTableRules({
|
|
29
|
+
tableName: config.tableName,
|
|
30
|
+
clientReq,
|
|
31
|
+
clientInfo,
|
|
32
|
+
scope,
|
|
33
|
+
resolvedPublishObject,
|
|
34
|
+
});
|
|
35
|
+
if (!fileRules?.select) return;
|
|
36
|
+
|
|
37
|
+
const versionHandler = this.dbo[versionTableName];
|
|
38
|
+
if (!versionHandler) throw new Error(`File version table not found: ${versionTableName}`);
|
|
39
|
+
const fileHandler = this.dbo[config.tableName];
|
|
40
|
+
if (!fileHandler) throw new Error(`File table not found: ${config.tableName}`);
|
|
41
|
+
const select = explicitRules?.select;
|
|
42
|
+
const requestedFields = versionHandler.parseFieldFilter(select?.fields ?? "*");
|
|
43
|
+
const fileSelectFields = fileHandler.parseFieldFilter(fileRules.select.fields);
|
|
44
|
+
const fields = (versionHandler.column_names as (keyof FileVersionTableRow)[]).filter(
|
|
45
|
+
(field) =>
|
|
46
|
+
requestedFields.includes(field) &&
|
|
47
|
+
fileSelectFields.includes(getFileVersionSourceField(field)) &&
|
|
48
|
+
(fileHandler.column_names as (keyof FileTableRow)[]).includes(
|
|
49
|
+
getFileVersionSourceField(field),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
const requestedFilterFields = versionHandler.parseFieldFilter(select?.filterFields ?? fields);
|
|
53
|
+
const requestedOrderByFields = versionHandler.parseFieldFilter(select?.orderByFields ?? fields);
|
|
54
|
+
const inheritedFilter = {
|
|
55
|
+
$existsJoined: {
|
|
56
|
+
path: [{ table: config.tableName, on: [{ file_id: "id" }] }],
|
|
57
|
+
filter: fileRules.select.forcedFilter ?? {},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
select: {
|
|
63
|
+
...fileRules.select,
|
|
64
|
+
...select,
|
|
65
|
+
fields: fromEntries(fields.map((field) => [field, 1 as const] as const)),
|
|
66
|
+
filterFields: fromEntries(
|
|
67
|
+
fields
|
|
68
|
+
.filter((field) => requestedFilterFields.includes(field))
|
|
69
|
+
.map((field) => [field, 1 as const] as const),
|
|
70
|
+
),
|
|
71
|
+
orderByFields: fromEntries(
|
|
72
|
+
fields
|
|
73
|
+
.filter((field) => requestedOrderByFields.includes(field))
|
|
74
|
+
.map((field) => [field, 1 as const] as const),
|
|
75
|
+
),
|
|
76
|
+
forcedFilter:
|
|
77
|
+
select?.forcedFilter ? { $and: [inheritedFilter, select.forcedFilter] } : inheritedFilter,
|
|
78
|
+
maxLimit: getMostRestrictiveLimit(fileRules.select.maxLimit, select?.maxLimit),
|
|
79
|
+
subscribeThrottle: Math.max(
|
|
80
|
+
fileRules.select.subscribeThrottle ?? 0,
|
|
81
|
+
select?.subscribeThrottle ?? 0,
|
|
82
|
+
),
|
|
83
|
+
disableMethods: {
|
|
84
|
+
...fileRules.select.disableMethods,
|
|
85
|
+
...select?.disableMethods,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getFileVersionSourceField = (field: keyof FileVersionTableRow): keyof FileTableRow => {
|
|
92
|
+
if (field === "file_id") return "id";
|
|
93
|
+
if (field === "created") return "updated";
|
|
94
|
+
return field;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getMostRestrictiveLimit = (
|
|
98
|
+
inheritedLimit: number | null | undefined,
|
|
99
|
+
explicitLimit: number | null | undefined,
|
|
100
|
+
) => {
|
|
101
|
+
const limits = [inheritedLimit, explicitLimit].filter(
|
|
102
|
+
(limit): limit is number => typeof limit === "number",
|
|
103
|
+
);
|
|
104
|
+
if (limits.length) return Math.min(...limits);
|
|
105
|
+
return inheritedLimit === null || explicitLimit === null ? null : undefined;
|
|
106
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isObject } from "prostgles-types";
|
|
2
2
|
import type { PublishParser } from "./PublishParser";
|
|
3
3
|
import { type PublishObject } from "./PublishParser";
|
|
4
|
+
import { getFileVersionTableName, isFileVersionTable } from "../StorageClient/fileVersionUtils";
|
|
4
5
|
|
|
5
6
|
export const getPublishedTableNames = (
|
|
6
7
|
publishParserInstance: PublishParser,
|
|
@@ -24,19 +25,27 @@ export const getPublishedTableNames = (
|
|
|
24
25
|
publishParserInstance.dbo[fileTableName]?.is_media &&
|
|
25
26
|
!tableNames.includes(fileTableName)
|
|
26
27
|
) {
|
|
27
|
-
const isReferenced = publishParserInstance.prostgles.dboBuilder.tablesOrViews?.some(
|
|
28
|
-
t
|
|
28
|
+
const isReferenced = publishParserInstance.prostgles.dboBuilder.tablesOrViews?.some(
|
|
29
|
+
(t) =>
|
|
30
|
+
!isFileVersionTable(publishParserInstance.prostgles.opts.fileTable, t.name) &&
|
|
31
|
+
t.columns.some((c) => c.references?.some((r) => r.ftable === fileTableName)),
|
|
29
32
|
);
|
|
30
33
|
if (isReferenced) {
|
|
31
34
|
tableNames.unshift(fileTableName);
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
const fileConfig = publishParserInstance.prostgles.opts.fileTable;
|
|
39
|
+
if (fileConfig?.versioning && tableNames.includes(fileConfig.tableName)) {
|
|
40
|
+
const versionTableName = getFileVersionTableName(fileConfig);
|
|
41
|
+
if (!tableNames.includes(versionTableName)) {
|
|
42
|
+
tableNames.unshift(versionTableName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
const audit = publishParserInstance.prostgles.resolvedAuditConfig;
|
|
36
47
|
const publishedFileTableName =
|
|
37
|
-
fileTableName && tableNames.includes(fileTableName) ?
|
|
38
|
-
fileTableName
|
|
39
|
-
: undefined;
|
|
48
|
+
fileTableName && tableNames.includes(fileTableName) ? fileTableName : undefined;
|
|
40
49
|
if (
|
|
41
50
|
audit &&
|
|
42
51
|
!tableNames.includes(audit.tableName) &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DB } from "../Prostgles";
|
|
2
2
|
import type { FileTableConfig } from "../ProstglesTypes";
|
|
3
|
+
import { getFileVersionTableName } from "./fileVersionUtils";
|
|
3
4
|
|
|
4
5
|
export const deleteUnreferencedFile = async (
|
|
5
6
|
db: DB,
|
|
@@ -11,8 +12,25 @@ export const deleteUnreferencedFile = async (
|
|
|
11
12
|
const referenced = await db.tx(async (tx) => {
|
|
12
13
|
await tx.none("SET LOCAL row_security = off");
|
|
13
14
|
return tx.oneOrNone<{ found: number }>(
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
`SELECT 1 AS found
|
|
16
|
+
WHERE EXISTS (
|
|
17
|
+
SELECT 1 FROM \${fileTable:name}
|
|
18
|
+
WHERE storage_key = \${key}::uuid OR (storage_key IS NULL AND id = \${key}::uuid)
|
|
19
|
+
)
|
|
20
|
+
${
|
|
21
|
+
config.versioning ?
|
|
22
|
+
`OR EXISTS (
|
|
23
|
+
SELECT 1 FROM \${versionTable:name}
|
|
24
|
+
WHERE storage_key = \${key}::uuid
|
|
25
|
+
)`
|
|
26
|
+
: ""
|
|
27
|
+
}
|
|
28
|
+
LIMIT 1`,
|
|
29
|
+
{
|
|
30
|
+
fileTable: config.tableName,
|
|
31
|
+
versionTable: getFileVersionTableName(config),
|
|
32
|
+
key: storageKey,
|
|
33
|
+
},
|
|
16
34
|
);
|
|
17
35
|
});
|
|
18
36
|
if (!referenced) await config.storageClient.delete(storageKey);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { omitKeys } from "prostgles-types";
|
|
2
|
+
import type { TableRowFromColumnDefinitions } from "../TableConfig/TableRowFromColumnDefinitions";
|
|
3
|
+
|
|
4
|
+
export const FILE_TABLE_COLUMN_DEFINITIONS = {
|
|
5
|
+
id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,
|
|
6
|
+
/**
|
|
7
|
+
* NULL for legacy objects stored under their file ID.
|
|
8
|
+
* */
|
|
9
|
+
storage_key: `UUID UNIQUE`,
|
|
10
|
+
extension: `TEXT NOT NULL DEFAULT ''`,
|
|
11
|
+
content_type: `TEXT NOT NULL DEFAULT ''`,
|
|
12
|
+
content_length: `BIGINT NOT NULL DEFAULT 0`,
|
|
13
|
+
etag: `TEXT NOT NULL DEFAULT ''`,
|
|
14
|
+
original_name: `TEXT NOT NULL`,
|
|
15
|
+
original_last_modified: `TIMESTAMPTZ`,
|
|
16
|
+
description: `TEXT`,
|
|
17
|
+
url: `TEXT NOT NULL DEFAULT ''`,
|
|
18
|
+
cloud_url: `TEXT`,
|
|
19
|
+
signed_url: `TEXT`,
|
|
20
|
+
signed_url_expires: `BIGINT`,
|
|
21
|
+
added: `TIMESTAMP NOT NULL DEFAULT NOW()`,
|
|
22
|
+
updated: `TIMESTAMP NOT NULL DEFAULT NOW()`,
|
|
23
|
+
deleted: `TIMESTAMPTZ`,
|
|
24
|
+
deleted_from_storage: `TIMESTAMPTZ`,
|
|
25
|
+
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
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const FILE_TABLE_VERSION_COLUMN_DEFINITIONS = {
|
|
29
|
+
version: `INTEGER NOT NULL DEFAULT 1`,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export const FILE_VERSION_TABLE_COLUMN_DEFINITIONS = {
|
|
33
|
+
...omitKeys(FILE_TABLE_COLUMN_DEFINITIONS, ["data"]),
|
|
34
|
+
storage_key: `UUID NOT NULL`,
|
|
35
|
+
file_id: `UUID NOT NULL`, // FK constraint is added later on
|
|
36
|
+
version: `INTEGER NOT NULL`,
|
|
37
|
+
created: `TIMESTAMP NOT NULL DEFAULT NOW()`,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
export type FileTableInsertRow = TableRowFromColumnDefinitions<
|
|
41
|
+
typeof FILE_TABLE_COLUMN_DEFINITIONS
|
|
42
|
+
> &
|
|
43
|
+
Partial<TableRowFromColumnDefinitions<typeof FILE_TABLE_VERSION_COLUMN_DEFINITIONS>>;
|
|
44
|
+
export type FileTableRow = Required<
|
|
45
|
+
TableRowFromColumnDefinitions<typeof FILE_TABLE_COLUMN_DEFINITIONS>
|
|
46
|
+
> &
|
|
47
|
+
Partial<Required<TableRowFromColumnDefinitions<typeof FILE_TABLE_VERSION_COLUMN_DEFINITIONS>>>;
|
|
48
|
+
|
|
49
|
+
export type FileVersionTableInsertRow = TableRowFromColumnDefinitions<
|
|
50
|
+
typeof FILE_VERSION_TABLE_COLUMN_DEFINITIONS
|
|
51
|
+
>;
|
|
52
|
+
export type FileVersionTableRow = Required<FileVersionTableInsertRow>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TableHandler } from "prostgles-types";
|
|
2
|
+
import type { TableHandlers } from "../DboBuilder/DboBuilder";
|
|
3
|
+
import type { FileTableConfig } from "../ProstglesTypes";
|
|
4
|
+
import type {
|
|
5
|
+
FileTableRow,
|
|
6
|
+
FileVersionTableInsertRow,
|
|
7
|
+
FileVersionTableRow,
|
|
8
|
+
} from "./fileTableDefinitions";
|
|
9
|
+
|
|
10
|
+
type FileVersionTableSchema = {
|
|
11
|
+
fileVersions: {
|
|
12
|
+
columns: FileVersionTableRow;
|
|
13
|
+
insertColumns: FileVersionTableInsertRow;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getFileVersionTableName = (config: FileTableConfig) =>
|
|
18
|
+
config.versioning?.tableName ?? `${config.tableName}_versions`;
|
|
19
|
+
|
|
20
|
+
export const isFileVersionTable = (
|
|
21
|
+
config: FileTableConfig | undefined,
|
|
22
|
+
tableName: string,
|
|
23
|
+
): config is FileTableConfig & { versioning: NonNullable<FileTableConfig["versioning"]> } =>
|
|
24
|
+
Boolean(config?.versioning && getFileVersionTableName(config) === tableName);
|
|
25
|
+
|
|
26
|
+
export const getFileTableHandler = (dbo: TableHandlers, config: FileTableConfig) => {
|
|
27
|
+
const handler = dbo[config.tableName] as TableHandler<FileTableRow> | undefined;
|
|
28
|
+
if (!handler) throw new Error(`File table not found: ${config.tableName}`);
|
|
29
|
+
return handler;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getFileRevisionsTableHandler = (dbo: TableHandlers, config: FileTableConfig) => {
|
|
33
|
+
const handler = dbo[getFileVersionTableName(config)] as
|
|
34
|
+
TableHandler<FileVersionTableRow, FileVersionTableSchema, "fileVersions"> | undefined;
|
|
35
|
+
if (!handler) throw new Error(`File table not found: ${config.tableName}`);
|
|
36
|
+
return handler;
|
|
37
|
+
};
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { FileTableRow } from "./getFileTableConfig";
|
|
2
2
|
|
|
3
3
|
/** Legacy files are stored under their logical ID. */
|
|
4
|
-
export const getFileStorageKey = (
|
|
5
|
-
file
|
|
4
|
+
export const getFileStorageKey = (
|
|
5
|
+
file: Pick<FileTableRow, "id" | "storage_key"> | { storage_key: string },
|
|
6
|
+
) => {
|
|
7
|
+
const { storage_key } = file;
|
|
8
|
+
const id = "id" in file ? file.id : undefined;
|
|
9
|
+
const key = storage_key ?? id;
|
|
10
|
+
if (!key) {
|
|
11
|
+
throw new Error("Unable to determine file storage key");
|
|
12
|
+
}
|
|
13
|
+
return key;
|
|
14
|
+
};
|
|
@@ -5,7 +5,10 @@ import { getFileTableConfig } from "./getFileTableConfig";
|
|
|
5
5
|
|
|
6
6
|
void test("managed files preserve hooks on other tables and append file hooks", () => {
|
|
7
7
|
const guard = { afterEach: [{ commands: { insert: 1 }, validate: async () => {} }] };
|
|
8
|
-
const fileGuard = {
|
|
8
|
+
const fileGuard = {
|
|
9
|
+
beforeEach: [{ commands: { insert: 1 }, validate: async () => {} }],
|
|
10
|
+
afterEach: [{ commands: { insert: 1 }, validate: async () => {} }],
|
|
11
|
+
};
|
|
9
12
|
const prostgles = {
|
|
10
13
|
opts: {
|
|
11
14
|
fileTable: { tableName: "files", storageClient: {}, expressApp: {} },
|
|
@@ -16,4 +19,6 @@ void test("managed files preserve hooks on other tables and append file hooks",
|
|
|
16
19
|
assert.equal(result.tableHooks?.documents, guard);
|
|
17
20
|
assert.equal(result.tableHooks.files?.beforeEach?.length, 2);
|
|
18
21
|
assert.equal(result.tableHooks.files.beforeEach[1], fileGuard.beforeEach[0]);
|
|
22
|
+
assert.equal(result.tableHooks.files.afterEach?.length, 2);
|
|
23
|
+
assert.equal(result.tableHooks.files.afterEach[0], fileGuard.afterEach[0]);
|
|
19
24
|
});
|