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.
Files changed (109) hide show
  1. package/dist/Audit/AuditTable.d.ts +2 -1
  2. package/dist/Audit/AuditTable.d.ts.map +1 -1
  3. package/dist/Audit/AuditTable.js.map +1 -1
  4. package/dist/Audit/getAuditTableRules.d.ts.map +1 -1
  5. package/dist/Audit/getAuditTableRules.js +3 -3
  6. package/dist/Audit/getAuditTableRules.js.map +1 -1
  7. package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.d.ts.map +1 -1
  8. package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.js +7 -1
  9. package/dist/DBSchemaBuilder/getClientDBGeneratedSchemas.js.map +1 -1
  10. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts +3 -1
  11. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.d.ts.map +1 -1
  12. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js +19 -5
  13. package/dist/DboBuilder/TableHandler/onDeleteFromFileTable.js.map +1 -1
  14. package/dist/DboBuilder/TableHandler/updateFile.d.ts +2 -39
  15. package/dist/DboBuilder/TableHandler/updateFile.d.ts.map +1 -1
  16. package/dist/DboBuilder/TableHandler/updateFile.js +1 -0
  17. package/dist/DboBuilder/TableHandler/updateFile.js.map +1 -1
  18. package/dist/DboBuilder/TableHandler/uploadFile.d.ts +2 -1
  19. package/dist/DboBuilder/TableHandler/uploadFile.d.ts.map +1 -1
  20. package/dist/DboBuilder/TableHandler/uploadFile.js +2 -1
  21. package/dist/DboBuilder/TableHandler/uploadFile.js.map +1 -1
  22. package/dist/DboBuilder/ViewHandler/getRawInfo.d.ts.map +1 -1
  23. package/dist/DboBuilder/ViewHandler/getRawInfo.js +6 -2
  24. package/dist/DboBuilder/ViewHandler/getRawInfo.js.map +1 -1
  25. package/dist/DboBuilder/getColumns.d.ts.map +1 -1
  26. package/dist/DboBuilder/getColumns.js +3 -0
  27. package/dist/DboBuilder/getColumns.js.map +1 -1
  28. package/dist/ProstglesTypes.d.ts +10 -0
  29. package/dist/ProstglesTypes.d.ts.map +1 -1
  30. package/dist/ProstglesTypes.js.map +1 -1
  31. package/dist/PublishParser/PublishParser.d.ts.map +1 -1
  32. package/dist/PublishParser/PublishParser.js +7 -0
  33. package/dist/PublishParser/PublishParser.js.map +1 -1
  34. package/dist/PublishParser/getFileTableRules.d.ts.map +1 -1
  35. package/dist/PublishParser/getFileTableRules.js +4 -1
  36. package/dist/PublishParser/getFileTableRules.js.map +1 -1
  37. package/dist/PublishParser/getFileVersionTableRules.d.ts +7 -0
  38. package/dist/PublishParser/getFileVersionTableRules.d.ts.map +1 -0
  39. package/dist/PublishParser/getFileVersionTableRules.js +77 -0
  40. package/dist/PublishParser/getFileVersionTableRules.js.map +1 -0
  41. package/dist/PublishParser/getFileVersionTableRules.spec.d.ts +2 -0
  42. package/dist/PublishParser/getFileVersionTableRules.spec.d.ts.map +1 -0
  43. package/dist/PublishParser/getFileVersionTableRules.spec.js +50 -0
  44. package/dist/PublishParser/getFileVersionTableRules.spec.js.map +1 -0
  45. package/dist/PublishParser/getPublishedTableNames.d.ts.map +1 -1
  46. package/dist/PublishParser/getPublishedTableNames.js +11 -4
  47. package/dist/PublishParser/getPublishedTableNames.js.map +1 -1
  48. package/dist/StorageClient/deleteUnreferencedFile.d.ts.map +1 -1
  49. package/dist/StorageClient/deleteUnreferencedFile.js +17 -1
  50. package/dist/StorageClient/deleteUnreferencedFile.js.map +1 -1
  51. package/dist/StorageClient/fileTableDefinitions.d.ts +54 -0
  52. package/dist/StorageClient/fileTableDefinitions.d.ts.map +1 -0
  53. package/dist/StorageClient/fileTableDefinitions.js +38 -0
  54. package/dist/StorageClient/fileTableDefinitions.js.map +1 -0
  55. package/dist/StorageClient/fileVersionUtils.d.ts +18 -0
  56. package/dist/StorageClient/fileVersionUtils.d.ts.map +1 -0
  57. package/dist/StorageClient/fileVersionUtils.js +22 -0
  58. package/dist/StorageClient/fileVersionUtils.js.map +1 -0
  59. package/dist/StorageClient/getFileStorageKey.d.ts +3 -1
  60. package/dist/StorageClient/getFileStorageKey.d.ts.map +1 -1
  61. package/dist/StorageClient/getFileStorageKey.js +9 -1
  62. package/dist/StorageClient/getFileStorageKey.js.map +1 -1
  63. package/dist/StorageClient/getFileTableConfig.d.ts +1 -23
  64. package/dist/StorageClient/getFileTableConfig.d.ts.map +1 -1
  65. package/dist/StorageClient/getFileTableConfig.js +63 -25
  66. package/dist/StorageClient/getFileTableConfig.js.map +1 -1
  67. package/dist/StorageClient/getFileTableConfig.spec.js +6 -1
  68. package/dist/StorageClient/getFileTableConfig.spec.js.map +1 -1
  69. package/dist/StorageClient/saveFileVersion.d.ts +7 -0
  70. package/dist/StorageClient/saveFileVersion.d.ts.map +1 -0
  71. package/dist/StorageClient/saveFileVersion.js +71 -0
  72. package/dist/StorageClient/saveFileVersion.js.map +1 -0
  73. package/dist/StorageClient/setupFileServeHandler.d.ts.map +1 -1
  74. package/dist/StorageClient/setupFileServeHandler.js +38 -21
  75. package/dist/StorageClient/setupFileServeHandler.js.map +1 -1
  76. package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts +11 -4
  77. package/dist/TableConfig/TableRowFromColumnDefinitions.d.ts.map +1 -1
  78. package/dist/TableConfig/initTableConfig.d.ts.map +1 -1
  79. package/dist/TableConfig/initTableConfig.js +9 -6
  80. package/dist/TableConfig/initTableConfig.js.map +1 -1
  81. package/dist/index.d.ts +2 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js.map +1 -1
  84. package/lib/Audit/AuditTable.ts +4 -1
  85. package/lib/Audit/getAuditTableRules.ts +5 -1
  86. package/lib/DBSchemaBuilder/getClientDBGeneratedSchemas.ts +9 -1
  87. package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +31 -5
  88. package/lib/DboBuilder/TableHandler/updateFile.ts +1 -0
  89. package/lib/DboBuilder/TableHandler/uploadFile.ts +3 -0
  90. package/lib/DboBuilder/ViewHandler/getRawInfo.ts +6 -2
  91. package/lib/DboBuilder/getColumns.ts +2 -0
  92. package/lib/ProstglesTypes.ts +11 -0
  93. package/lib/PublishParser/PublishParser.ts +25 -7
  94. package/lib/PublishParser/getFileTableRules.ts +7 -1
  95. package/lib/PublishParser/getFileVersionTableRules.spec.ts +57 -0
  96. package/lib/PublishParser/getFileVersionTableRules.ts +106 -0
  97. package/lib/PublishParser/getPublishedTableNames.ts +14 -5
  98. package/lib/StorageClient/deleteUnreferencedFile.ts +20 -2
  99. package/lib/StorageClient/fileTableDefinitions.ts +52 -0
  100. package/lib/StorageClient/fileVersionUtils.ts +37 -0
  101. package/lib/StorageClient/getFileStorageKey.ts +11 -2
  102. package/lib/StorageClient/getFileTableConfig.spec.ts +6 -1
  103. package/lib/StorageClient/getFileTableConfig.ts +85 -30
  104. package/lib/StorageClient/saveFileVersion.ts +82 -0
  105. package/lib/StorageClient/setupFileServeHandler.ts +51 -28
  106. package/lib/TableConfig/TableRowFromColumnDefinitions.ts +37 -13
  107. package/lib/TableConfig/initTableConfig.ts +10 -6
  108. package/lib/index.ts +2 -1
  109. 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) || reservedNames.has(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 result = files.map(({ _prostgles_storage_key, ...row }) => {
36
- onCommit(({ db }) => deleteUnreferencedFile(db, config, _prostgles_storage_key));
37
- return row;
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
  };
@@ -33,6 +33,7 @@ export const updateFile = async (
33
33
  data,
34
34
  localParams,
35
35
  mediaId: existingFile.id,
36
+ version: Number(existingFile.version ?? 1) + 1,
36
37
  original_last_modified,
37
38
  });
38
39
  return { newData: omitKeys(newFile, ["id"]) };
@@ -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 fileTableName = this.dboBuilder.prostgles.opts.fileTable?.tableName;
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) =>
@@ -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
- await this.getParsedPublishTable({
318
- clientReq,
319
- tableName,
320
- clientInfo,
321
- resolvedPublishObject,
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((t) => !t.is_view && t.name !== fileTableName)
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((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
  });