prostgles-server 4.2.625 → 4.2.627

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/debug/prostgles-types-4.0.285.tgz +0 -0
  2. package/dist/Audit/AuditTable.d.ts +2 -1
  3. package/dist/Audit/AuditTable.d.ts.map +1 -1
  4. package/dist/Audit/AuditTable.js.map +1 -1
  5. package/dist/Audit/getAuditTableRules.d.ts.map +1 -1
  6. package/dist/Audit/getAuditTableRules.js +4 -4
  7. package/dist/Audit/getAuditTableRules.js.map +1 -1
  8. package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.d.ts.map +1 -1
  9. package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.js +7 -1
  10. package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.js.map +1 -1
  11. package/dist/DboBuilder/QueryBuilder/Functions/Functions.d.ts +3 -1
  12. package/dist/DboBuilder/QueryBuilder/Functions/Functions.d.ts.map +1 -1
  13. package/dist/DboBuilder/QueryBuilder/Functions/Functions.js +18 -11
  14. package/dist/DboBuilder/QueryBuilder/Functions/Functions.js.map +1 -1
  15. package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.d.ts.map +1 -1
  16. package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.js +16 -21
  17. package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.js.map +1 -1
  18. package/dist/DboBuilder/QueryBuilder/Functions/utils.d.ts +1 -0
  19. package/dist/DboBuilder/QueryBuilder/Functions/utils.d.ts.map +1 -1
  20. package/dist/DboBuilder/QueryBuilder/Functions/utils.js +3 -1
  21. package/dist/DboBuilder/QueryBuilder/Functions/utils.js.map +1 -1
  22. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts +18 -2
  23. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts.map +1 -1
  24. package/dist/DboBuilder/QueryBuilder/QueryBuilder.js +54 -16
  25. package/dist/DboBuilder/QueryBuilder/QueryBuilder.js.map +1 -1
  26. package/dist/DboBuilder/QueryBuilder/getNewQuery.d.ts +1 -1
  27. package/dist/DboBuilder/QueryBuilder/getNewQuery.d.ts.map +1 -1
  28. package/dist/DboBuilder/QueryBuilder/getNewQuery.js +40 -6
  29. package/dist/DboBuilder/QueryBuilder/getNewQuery.js.map +1 -1
  30. package/dist/DboBuilder/QueryBuilder/parseJoinSelect.d.ts.map +1 -1
  31. package/dist/DboBuilder/QueryBuilder/parseJoinSelect.js +2 -12
  32. package/dist/DboBuilder/QueryBuilder/parseJoinSelect.js.map +1 -1
  33. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts +3 -1
  34. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts.map +1 -1
  35. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js +21 -6
  36. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js.map +1 -1
  37. package/dist/DboBuilder/TableHandler/updateFile.d.ts +2 -39
  38. package/dist/DboBuilder/TableHandler/updateFile.d.ts.map +1 -1
  39. package/dist/DboBuilder/TableHandler/updateFile.js +1 -0
  40. package/dist/DboBuilder/TableHandler/updateFile.js.map +1 -1
  41. package/dist/DboBuilder/TableHandler/uploadFile.d.ts +2 -1
  42. package/dist/DboBuilder/TableHandler/uploadFile.d.ts.map +1 -1
  43. package/dist/DboBuilder/TableHandler/uploadFile.js +2 -1
  44. package/dist/DboBuilder/TableHandler/uploadFile.js.map +1 -1
  45. package/dist/DboBuilder/ViewHandler/find.d.ts.map +1 -1
  46. package/dist/DboBuilder/ViewHandler/find.js +2 -29
  47. package/dist/DboBuilder/ViewHandler/find.js.map +1 -1
  48. package/dist/DboBuilder/ViewHandler/getRawInfo.d.ts.map +1 -1
  49. package/dist/DboBuilder/ViewHandler/getRawInfo.js +6 -2
  50. package/dist/DboBuilder/ViewHandler/getRawInfo.js.map +1 -1
  51. package/dist/DboBuilder/ViewHandler/parseComplexFilter.d.ts +4 -1
  52. package/dist/DboBuilder/ViewHandler/parseComplexFilter.d.ts.map +1 -1
  53. package/dist/DboBuilder/ViewHandler/parseComplexFilter.js +9 -2
  54. package/dist/DboBuilder/ViewHandler/parseComplexFilter.js.map +1 -1
  55. package/dist/DboBuilder/ViewHandler/subscribe.d.ts.map +1 -1
  56. package/dist/DboBuilder/ViewHandler/subscribe.js +8 -3
  57. package/dist/DboBuilder/ViewHandler/subscribe.js.map +1 -1
  58. package/dist/DboBuilder/ViewHandler/validateSelectParams.d.ts +3 -0
  59. package/dist/DboBuilder/ViewHandler/validateSelectParams.d.ts.map +1 -0
  60. package/dist/DboBuilder/ViewHandler/validateSelectParams.js +48 -0
  61. package/dist/DboBuilder/ViewHandler/validateSelectParams.js.map +1 -0
  62. package/dist/DboBuilder/getColumns.d.ts.map +1 -1
  63. package/dist/DboBuilder/getColumns.js +3 -0
  64. package/dist/DboBuilder/getColumns.js.map +1 -1
  65. package/dist/DboBuilder/getCondition.d.ts.map +1 -1
  66. package/dist/DboBuilder/getCondition.js +9 -3
  67. package/dist/DboBuilder/getCondition.js.map +1 -1
  68. package/dist/DboBuilder/getSubscribeRelatedTables.d.ts.map +1 -1
  69. package/dist/DboBuilder/getSubscribeRelatedTables.js +7 -2
  70. package/dist/DboBuilder/getSubscribeRelatedTables.js.map +1 -1
  71. package/dist/ProstglesTypes.d.ts +10 -0
  72. package/dist/ProstglesTypes.d.ts.map +1 -1
  73. package/dist/ProstglesTypes.js.map +1 -1
  74. package/dist/PubSubManager/PubSubManager.d.ts +1 -1
  75. package/dist/PubSubManager/PubSubManager.d.ts.map +1 -1
  76. package/dist/PubSubManager/PubSubManager.js +1 -1
  77. package/dist/PubSubManager/PubSubManager.js.map +1 -1
  78. package/dist/PubSubManager/addSub.js +1 -1
  79. package/dist/PubSubManager/addSub.js.map +1 -1
  80. package/dist/PublishParser/PublishParser.d.ts.map +1 -1
  81. package/dist/PublishParser/PublishParser.js +7 -0
  82. package/dist/PublishParser/PublishParser.js.map +1 -1
  83. package/dist/PublishParser/getFileTableRules.d.ts.map +1 -1
  84. package/dist/PublishParser/getFileTableRules.js +4 -1
  85. package/dist/PublishParser/getFileTableRules.js.map +1 -1
  86. package/dist/PublishParser/getFileVersionTableRules.d.ts +7 -0
  87. package/dist/PublishParser/getFileVersionTableRules.d.ts.map +1 -0
  88. package/dist/PublishParser/getFileVersionTableRules.js +77 -0
  89. package/dist/PublishParser/getFileVersionTableRules.js.map +1 -0
  90. package/dist/PublishParser/getFileVersionTableRules.spec.d.ts +2 -0
  91. package/dist/PublishParser/getFileVersionTableRules.spec.d.ts.map +1 -0
  92. package/dist/PublishParser/getFileVersionTableRules.spec.js +50 -0
  93. package/dist/PublishParser/getFileVersionTableRules.spec.js.map +1 -0
  94. package/dist/PublishParser/getPublishedTableNames.d.ts.map +1 -1
  95. package/dist/PublishParser/getPublishedTableNames.js +11 -4
  96. package/dist/PublishParser/getPublishedTableNames.js.map +1 -1
  97. package/dist/StorageClient/deleteUnreferencedFile.d.ts.map +1 -1
  98. package/dist/StorageClient/deleteUnreferencedFile.js +17 -1
  99. package/dist/StorageClient/deleteUnreferencedFile.js.map +1 -1
  100. package/dist/StorageClient/fileTableDefinitions.d.ts +54 -0
  101. package/dist/StorageClient/fileTableDefinitions.d.ts.map +1 -0
  102. package/dist/StorageClient/fileTableDefinitions.js +38 -0
  103. package/dist/StorageClient/fileTableDefinitions.js.map +1 -0
  104. package/dist/StorageClient/fileVersionUtils.d.ts +18 -0
  105. package/dist/StorageClient/fileVersionUtils.d.ts.map +1 -0
  106. package/dist/StorageClient/fileVersionUtils.js +22 -0
  107. package/dist/StorageClient/fileVersionUtils.js.map +1 -0
  108. package/dist/StorageClient/getFileStorageKey.d.ts +3 -1
  109. package/dist/StorageClient/getFileStorageKey.d.ts.map +1 -1
  110. package/dist/StorageClient/getFileStorageKey.js +9 -1
  111. package/dist/StorageClient/getFileStorageKey.js.map +1 -1
  112. package/dist/StorageClient/getFileTableConfig.d.ts +1 -23
  113. package/dist/StorageClient/getFileTableConfig.d.ts.map +1 -1
  114. package/dist/StorageClient/getFileTableConfig.js +63 -25
  115. package/dist/StorageClient/getFileTableConfig.js.map +1 -1
  116. package/dist/StorageClient/getFileTableConfig.spec.js +6 -1
  117. package/dist/StorageClient/getFileTableConfig.spec.js.map +1 -1
  118. package/dist/StorageClient/saveFileVersion.d.ts +7 -0
  119. package/dist/StorageClient/saveFileVersion.d.ts.map +1 -0
  120. package/dist/StorageClient/saveFileVersion.js +71 -0
  121. package/dist/StorageClient/saveFileVersion.js.map +1 -0
  122. package/dist/StorageClient/setupFileServeHandler.d.ts.map +1 -1
  123. package/dist/StorageClient/setupFileServeHandler.js +38 -21
  124. package/dist/StorageClient/setupFileServeHandler.js.map +1 -1
  125. package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts +11 -4
  126. package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts.map +1 -1
  127. package/dist/TableConfig/initTableConfig.d.ts.map +1 -1
  128. package/dist/TableConfig/initTableConfig.js +6 -6
  129. package/dist/TableConfig/initTableConfig.js.map +1 -1
  130. package/dist/index.d.ts +2 -2
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js.map +1 -1
  133. package/lib/Audit/AuditTable.ts +4 -1
  134. package/lib/Audit/getAuditTableRules.ts +6 -2
  135. package/lib/DBSchemaBuilder/getClientDBGeneratedSchemas.ts +9 -1
  136. package/lib/DboBuilder/QueryBuilder/Functions/Functions.ts +38 -13
  137. package/lib/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.ts +28 -24
  138. package/lib/DboBuilder/QueryBuilder/Functions/utils.ts +10 -0
  139. package/lib/DboBuilder/QueryBuilder/QueryBuilder.ts +95 -18
  140. package/lib/DboBuilder/QueryBuilder/getNewQuery.ts +58 -10
  141. package/lib/DboBuilder/QueryBuilder/parseJoinSelect.ts +2 -13
  142. package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +34 -7
  143. package/lib/DboBuilder/TableHandler/updateFile.ts +1 -0
  144. package/lib/DboBuilder/TableHandler/uploadFile.ts +3 -0
  145. package/lib/DboBuilder/ViewHandler/find.ts +2 -32
  146. package/lib/DboBuilder/ViewHandler/getRawInfo.ts +6 -2
  147. package/lib/DboBuilder/ViewHandler/parseComplexFilter.ts +10 -2
  148. package/lib/DboBuilder/ViewHandler/subscribe.ts +10 -3
  149. package/lib/DboBuilder/ViewHandler/validateSelectParams.ts +51 -0
  150. package/lib/DboBuilder/getColumns.ts +2 -0
  151. package/lib/DboBuilder/getCondition.ts +11 -3
  152. package/lib/DboBuilder/getSubscribeRelatedTables.ts +10 -3
  153. package/lib/ProstglesTypes.ts +11 -0
  154. package/lib/PubSubManager/PubSubManager.ts +1 -1
  155. package/lib/PubSubManager/addSub.ts +1 -1
  156. package/lib/PublishParser/PublishParser.ts +25 -7
  157. package/lib/PublishParser/getFileTableRules.ts +7 -1
  158. package/lib/PublishParser/getFileVersionTableRules.spec.ts +57 -0
  159. package/lib/PublishParser/getFileVersionTableRules.ts +106 -0
  160. package/lib/PublishParser/getPublishedTableNames.ts +14 -5
  161. package/lib/StorageClient/deleteUnreferencedFile.ts +20 -2
  162. package/lib/StorageClient/fileTableDefinitions.ts +52 -0
  163. package/lib/StorageClient/fileVersionUtils.ts +37 -0
  164. package/lib/StorageClient/getFileStorageKey.ts +11 -2
  165. package/lib/StorageClient/getFileTableConfig.spec.ts +6 -1
  166. package/lib/StorageClient/getFileTableConfig.ts +85 -30
  167. package/lib/StorageClient/saveFileVersion.ts +82 -0
  168. package/lib/StorageClient/setupFileServeHandler.ts +51 -28
  169. package/lib/TableConfig/TableRowFromColumnDefinitions.ts +37 -13
  170. package/lib/TableConfig/initTableConfig.ts +5 -6
  171. package/lib/index.ts +2 -1
  172. package/package.json +2 -2
@@ -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((t) =>
28
- t.columns.some((c) => c.references?.some((r) => r.ftable === fileTableName)),
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
- "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 },
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 = (file: Pick<FileTableRow, "id" | "storage_key">) =>
5
- file.storage_key ?? file.id;
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 = { beforeEach: [{ commands: { insert: 1 }, validate: async () => {} }] };
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
  });
@@ -1,37 +1,27 @@
1
- import { omitKeys } from "prostgles-types";
1
+ import { asName, omitKeys } from "prostgles-types";
2
+ import { md5 } from "prostgles-types/dist/md5";
2
3
  import { onDeleteFromFileTable } from "../DboBuilder/TableHandler/onDeleteFromFileTable";
3
4
  import { updateFile } from "../DboBuilder/TableHandler/updateFile";
4
5
  import { assertFileObjectValid, uploadFile } from "../DboBuilder/TableHandler/uploadFile";
5
6
  import type { Prostgles } from "../Prostgles";
6
- import type { BeforeEachTsTrigger } from "../PublishParser/publishTypesAndUtils";
7
+ import type {
8
+ AfterEachTsTrigger,
9
+ BeforeEachTsTrigger,
10
+ } from "../PublishParser/publishTypesAndUtils";
7
11
  import type { TableConfig } from "../TableConfig/TableConfigTypes";
8
- import type { TableRowFromColumnDefinitions } from "../TableConfig/TableRowFromColumnDefinitions";
9
12
  import type { TableHooks } from "../TableHooks/TableHooks";
10
13
  import { setupFileServeHandler } from "./setupFileServeHandler";
11
14
  import type { TableHandlers } from "../DboBuilder/DboBuilder";
15
+ import { getFileTableHandler, getFileVersionTableName } from "./fileVersionUtils";
16
+ import { backfillFileVersions, saveFileVersion } from "./saveFileVersion";
17
+ import {
18
+ FILE_TABLE_COLUMN_DEFINITIONS,
19
+ FILE_TABLE_VERSION_COLUMN_DEFINITIONS,
20
+ FILE_VERSION_TABLE_COLUMN_DEFINITIONS,
21
+ type FileTableRow,
22
+ } from "./fileTableDefinitions";
12
23
 
13
- const FILE_TABLE_COLUMN_DEFINITIONS = {
14
- id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,
15
- storage_key: `UUID UNIQUE`, // NULL for legacy objects stored under their file ID.
16
- extension: `TEXT NOT NULL DEFAULT ''`,
17
- content_type: `TEXT NOT NULL DEFAULT ''`,
18
- content_length: `BIGINT NOT NULL DEFAULT 0`,
19
- etag: `TEXT NOT NULL DEFAULT ''`,
20
- original_name: `TEXT NOT NULL`,
21
- original_last_modified: `TIMESTAMPTZ`,
22
- description: `TEXT`,
23
- url: `TEXT NOT NULL DEFAULT ''`,
24
- cloud_url: `TEXT`,
25
- signed_url: `TEXT`,
26
- signed_url_expires: `BIGINT`,
27
- added: `TIMESTAMP NOT NULL DEFAULT NOW()`,
28
- updated: `TIMESTAMP NOT NULL DEFAULT NOW()`,
29
- deleted: `TIMESTAMPTZ`,
30
- deleted_from_storage: `TIMESTAMPTZ`,
31
- 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
32
- } as const;
33
-
34
- export type FileTableRow = TableRowFromColumnDefinitions<typeof FILE_TABLE_COLUMN_DEFINITIONS>;
24
+ export type { FileTableInsertRow, FileTableRow } from "./fileTableDefinitions";
35
25
 
36
26
  export const getFileTableConfig = (
37
27
  prg: Prostgles,
@@ -44,7 +34,30 @@ export const getFileTableConfig = (
44
34
  const { expressApp } = fileTable;
45
35
 
46
36
  const { tableName: fileTableName, storageClient } = fileTable;
37
+ const versionTableName = getFileVersionTableName(fileTable);
38
+ const versionConstraintName = `prostgles_file_version_${md5(JSON.stringify([versionTableName, fileTableName]))}`;
39
+ const versionIndexName = `prostgles_file_version_${md5(versionTableName)}_key`;
40
+ if (fileTable.versioning) {
41
+ if (versionTableName === fileTableName) {
42
+ throw new Error("File version table name must differ from the file table name");
43
+ }
44
+ if (
45
+ fileTable.versioning.maxVersions !== undefined &&
46
+ (!Number.isInteger(fileTable.versioning.maxVersions) || fileTable.versioning.maxVersions < 1)
47
+ ) {
48
+ throw new Error("fileTable.versioning.maxVersions must be a positive integer");
49
+ }
50
+ if (tableConfig?.[versionTableName]) {
51
+ throw new Error(
52
+ `File version table name (${versionTableName}) is managed and cannot be configured in tableConfig`,
53
+ );
54
+ }
55
+ }
47
56
 
57
+ const fileColumnDefinitions = {
58
+ ...FILE_TABLE_COLUMN_DEFINITIONS,
59
+ ...(fileTable.versioning && FILE_TABLE_VERSION_COLUMN_DEFINITIONS),
60
+ };
48
61
  const userFileTableConfig = tableConfig?.[fileTableName];
49
62
  if (userFileTableConfig) {
50
63
  if ("isLookupTable" in userFileTableConfig) {
@@ -59,12 +72,21 @@ export const getFileTableConfig = (
59
72
  }
60
73
  if ("columns" in userFileTableConfig && userFileTableConfig.columns) {
61
74
  const userCols = new Set(Object.keys(userFileTableConfig.columns));
62
- const clashingColumns = Object.keys(FILE_TABLE_COLUMN_DEFINITIONS).filter((col) =>
75
+ const clashingFileColumns = Object.keys(fileColumnDefinitions).filter((col) =>
63
76
  userCols.has(col),
64
77
  );
65
- if (clashingColumns.length) {
78
+ if (clashingFileColumns.length) {
79
+ throw new Error(
80
+ `FileManager table name (${fileTableName}) has clashing column names in tableConfig: ${clashingFileColumns}`,
81
+ );
82
+ }
83
+
84
+ const clashingFileRevisionColumns = Object.keys(FILE_VERSION_TABLE_COLUMN_DEFINITIONS).filter(
85
+ (col) => userCols.has(col),
86
+ );
87
+ if (clashingFileRevisionColumns.length) {
66
88
  throw new Error(
67
- `FileManager table name (${fileTableName}) has clashing column names in tableConfig: ${clashingColumns}`,
89
+ `FileManager table name (${fileTableName}) has clashing column names with file revision table in tableConfig: ${clashingFileRevisionColumns}`,
68
90
  );
69
91
  }
70
92
  }
@@ -74,7 +96,7 @@ export const getFileTableConfig = (
74
96
  [fileTableName]: {
75
97
  ...userFileTableConfig,
76
98
  columns: {
77
- ...FILE_TABLE_COLUMN_DEFINITIONS,
99
+ ...fileColumnDefinitions,
78
100
  ...userFileTableConfig?.columns,
79
101
  },
80
102
  onMount: ({ _db }) => {
@@ -89,6 +111,24 @@ export const getFileTableConfig = (
89
111
  };
90
112
  },
91
113
  },
114
+ ...(fileTable.versioning && {
115
+ [versionTableName]: {
116
+ /** Mirror user columns in the file revision table */
117
+ columns: { ...FILE_VERSION_TABLE_COLUMN_DEFINITIONS, ...userFileTableConfig?.columns },
118
+ constraints: {
119
+ [versionConstraintName]: `FOREIGN KEY (file_id) REFERENCES ${asName(fileTableName)}(id) ON DELETE CASCADE`,
120
+ },
121
+ indexes: {
122
+ [versionIndexName]: {
123
+ unique: true,
124
+ columns: "file_id, version",
125
+ },
126
+ },
127
+ onMount: async ({ dbo }) => {
128
+ await backfillFileVersions(dbo, fileTable);
129
+ },
130
+ },
131
+ }),
92
132
  ...omitKeys(tableConfig ?? {}, [fileTableName]),
93
133
  };
94
134
 
@@ -156,9 +196,24 @@ export const getFileTableConfig = (
156
196
  } satisfies BeforeEachTsTrigger<FileTableRow, TableHandlers>,
157
197
  ...(userFileTableHooks?.beforeEach || []),
158
198
  ],
159
- onInsteadOfDelete: async ({ onCommit, tx, returningQuery, isOneOrNone, filterOpts }) => {
199
+ afterEach: [
200
+ ...(userFileTableHooks?.afterEach || []),
201
+ {
202
+ commands: { insert: 1, update: 1 },
203
+ validate: async ({ row, dbx, onCommit }) => {
204
+ const fileRow = await getFileTableHandler(dbx, fileTable).findOne({ id: row.id });
205
+ if (!fileRow) throw new Error(`File not found after write: ${row.id}`);
206
+ await saveFileVersion(fileTable, fileRow, dbx, onCommit);
207
+ },
208
+ } satisfies AfterEachTsTrigger<FileTableRow, TableHandlers> as AfterEachTsTrigger<
209
+ any,
210
+ TableHandlers
211
+ >,
212
+ ],
213
+ onInsteadOfDelete: async ({ onCommit, dbx, tx, returningQuery, isOneOrNone, filterOpts }) => {
160
214
  return onDeleteFromFileTable(fileTable, {
161
215
  onCommit,
216
+ dbx,
162
217
  t: tx,
163
218
  returningQuery,
164
219
  isOneOrNone,
@@ -0,0 +1,82 @@
1
+ import { pickKeys } from "prostgles-types";
2
+ import type { TableHandlers } from "../DboBuilder/DboBuilder";
3
+ import type { TableHandler } from "../DboBuilder/TableHandler/TableHandler";
4
+ import type { FileTableConfig } from "../ProstglesTypes";
5
+ import type { OnCommit } from "../PublishParser/publishTypesAndUtils";
6
+ import { deleteUnreferencedFile } from "./deleteUnreferencedFile";
7
+ import type { FileTableRow, FileVersionTableInsertRow } from "./fileTableDefinitions";
8
+ import { getFileRevisionsTableHandler, getFileTableHandler } from "./fileVersionUtils";
9
+ import { getFileStorageKey } from "./getFileStorageKey";
10
+
11
+ export const saveFileVersion = async (
12
+ config: FileTableConfig,
13
+ row: FileTableRow,
14
+ dbx: TableHandlers,
15
+ onCommit: OnCommit,
16
+ ) => {
17
+ if (!config.versioning) return;
18
+ const version = Number(row.version);
19
+ if (!Number.isInteger(version) || version < 1) {
20
+ throw new Error(`Invalid file version: ${row.version}`);
21
+ }
22
+
23
+ const versions = getFileRevisionsTableHandler(dbx, config);
24
+ const versionColumns = (versions as TableHandler).column_names;
25
+ await versions.insert(getFileVersionRow(row, versionColumns), {
26
+ onConflict: {
27
+ action: "DoNothing",
28
+ conflictColumns: ["file_id", "version"],
29
+ },
30
+ });
31
+
32
+ const { maxVersions } = config.versioning;
33
+ if (maxVersions === undefined) return;
34
+ const removed = (await versions.find(
35
+ { file_id: row.id },
36
+ {
37
+ select: { id: 1, storage_key: 1 },
38
+ orderBy: { version: -1 },
39
+ offset: maxVersions,
40
+ limit: null,
41
+ },
42
+ )) as { id: string; storage_key: string }[];
43
+ if (!removed.length) return;
44
+
45
+ await versions.delete({ id: { $in: removed.map(({ id }) => id) } });
46
+ for (const storageKey of new Set(removed.map(({ storage_key }) => storage_key))) {
47
+ onCommit(({ db }) => deleteUnreferencedFile(db, config, storageKey));
48
+ }
49
+ };
50
+
51
+ export const backfillFileVersions = async (dbo: TableHandlers, config: FileTableConfig) => {
52
+ if (!config.versioning) return;
53
+ const files = getFileTableHandler(dbo, config);
54
+ const fileRows = await files.find({}, { limit: null });
55
+ if (!fileRows.length) return;
56
+ console.log(`Backfilling file versions for ${fileRows.length} files`);
57
+ const fileRevisionsTableHandler = getFileRevisionsTableHandler(dbo, config);
58
+ const fileRevisionColumns = (fileRevisionsTableHandler as TableHandler).column_names;
59
+ await fileRevisionsTableHandler.insertMany(
60
+ fileRows.map((fileRow) => getFileVersionRow(fileRow, fileRevisionColumns)),
61
+ {
62
+ onConflict: {
63
+ action: "DoNothing",
64
+ conflictColumns: ["file_id", "version"],
65
+ },
66
+ },
67
+ );
68
+ };
69
+
70
+ /** Must ensure we copy over any columns that might have been created by the user */
71
+ const getFileVersionRow = (row: FileTableRow, cols: string[]): FileVersionTableInsertRow => {
72
+ const { id, ...insertRow } = pickKeys(row, cols as (keyof FileTableRow)[]);
73
+ const version = Number(row.version ?? 1);
74
+ return {
75
+ ...insertRow,
76
+ file_id: id,
77
+ version,
78
+ storage_key: getFileStorageKey(row),
79
+ url: `${row.url}?version=${version}`,
80
+ created: row.updated,
81
+ };
82
+ };