graphile-presigned-url-plugin 1.16.0 → 1.17.0

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.
@@ -24,6 +24,7 @@ import { getFileRefFieldBinding } from './file-ref-registry';
24
24
  import { provisionAndRecordPhysicalBucket, resolveS3ForDatabase } from './physical-bucket';
25
25
  import { withRequestPgClient } from './request-pg-client';
26
26
  import { copyS3Object, deleteS3Object } from './s3-signer';
27
+ import { recordManagedFile } from './storage-file-recorder';
27
28
  import { getBucketConfig, loadAllStorageModules } from './storage-module-cache';
28
29
  const log = new Logger('graphile-presigned-url:managed-upload');
29
30
  /**
@@ -252,22 +253,14 @@ export async function finalizeStagedUpload(args) {
252
253
  let fileId;
253
254
  try {
254
255
  fileId = await withRequestPgClient(withPgClient, pgSettings, async (pgClient) => {
255
- const result = await pgClient.query({
256
- text: `INSERT INTO ${storageConfig.filesQualifiedName}
257
- (bucket_id, key, content_hash, mime_type, size, filename, is_public)
258
- VALUES ($1, $2, $3, $4, $5, $6, $7)
259
- RETURNING id`,
260
- values: [
261
- bucket.id,
262
- finalKey,
263
- staged.contentHash,
264
- staged.contentType,
265
- staged.size,
266
- staged.filename ?? null,
267
- bucket.is_public,
268
- ],
256
+ return recordManagedFile(pgClient, storageConfig, {
257
+ bucketId: bucket.id,
258
+ key: finalKey,
259
+ contentHash: staged.contentHash,
260
+ mimeType: staged.contentType,
261
+ size: staged.size,
262
+ filename: staged.filename,
269
263
  });
270
- return result.rows[0].id;
271
264
  });
272
265
  }
273
266
  catch (err) {
@@ -9,6 +9,7 @@
9
9
  * environment-level bucket standing in for a tenant's.
10
10
  */
11
11
  import { Logger } from '@pgpmjs/logger';
12
+ import { recordPhysicalName } from 'graphile-storage-registry';
12
13
  import { withRequestPgClient } from './request-pg-client';
13
14
  import { s3FailureError } from './s3-failure';
14
15
  import { isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
@@ -108,12 +109,7 @@ export async function provisionAndRecordPhysicalBucket(options, withPgClient, st
108
109
  // guard keeps this idempotent and race-safe across concurrent first uploads.
109
110
  // The catalog-sync trigger on this UPDATE needs `jwt.claims.database_id`, so the
110
111
  // write runs under the resolved database claim (privileged role preserved).
111
- await withRequestPgClient(withPgClient, { 'jwt.claims.database_id': databaseId }, (client) => client.query({
112
- text: `UPDATE ${storageConfig.bucketsQualifiedName}
113
- SET physical_name = $1
114
- WHERE id = $2 AND physical_name IS NULL`,
115
- values: [s3BucketName, bucket.id],
116
- }));
112
+ await withRequestPgClient(withPgClient, { 'jwt.claims.database_id': databaseId }, (client) => recordPhysicalName((query) => client.query(query), storageConfig.bucketsQualifiedName, bucket.id, s3BucketName));
117
113
  bucket.physical_name = s3BucketName;
118
114
  log.info(`Recorded physical_name="${s3BucketName}" on bucket ${bucket.id}`);
119
115
  return s3BucketName;
package/esm/plugin.js CHANGED
@@ -30,6 +30,7 @@ import { buildFileProjection } from './managed-upload';
30
30
  import { provisionAndRecordPhysicalBucket, resolveS3ForDatabase } from './physical-bucket';
31
31
  import { withRequestPgClient } from './request-pg-client';
32
32
  import { deleteS3Object, generatePresignedPutUrl } from './s3-signer';
33
+ import { recordManagedFile } from './storage-file-recorder';
33
34
  import { getBucketConfig, loadAllStorageModules, resolveStorageConfigFromCodec, storedPhysicalName } from './storage-module-cache';
34
35
  const log = new Logger('graphile-presigned-url:plugin');
35
36
  // --- Protocol-level constants (not configurable) ---
@@ -612,32 +613,16 @@ async function processSingleFile(options, txClient, storageConfig, databaseId, b
612
613
  }
613
614
  // Auto-derive ltree path from custom key directory (only when has_path_shares)
614
615
  const derivedPath = isCustomKey && storageConfig.hasPathShares ? derivePathFromKey(s3Key) : null;
615
- // Create file record. An entity-keyed plane (the module records an entity
616
- // table) carries owner_id on its rows; app- and database-scope planes do not.
617
- const hasOwnerColumn = storageConfig.entityTableId !== null;
618
- const columns = ['bucket_id', 'key', 'content_hash', 'mime_type', 'size', 'filename', 'is_public'];
619
- const values = [bucket.id, s3Key, contentHash, contentType, size, filename || null, bucket.is_public];
620
- if (hasOwnerColumn) {
621
- columns.push('owner_id');
622
- values.push(bucket.owner_id);
623
- }
624
- if (previousVersionId) {
625
- columns.push('previous_version_id');
626
- values.push(previousVersionId);
627
- }
628
- if (derivedPath) {
629
- columns.push('path');
630
- values.push(derivedPath);
631
- }
632
- const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
633
- const fileResult = await txClient.query({
634
- text: `INSERT INTO ${storageConfig.filesQualifiedName}
635
- (${columns.join(', ')})
636
- VALUES (${placeholders})
637
- RETURNING id`,
638
- values,
616
+ const fileId = await recordManagedFile(txClient, storageConfig, {
617
+ bucketId: bucket.id,
618
+ key: s3Key,
619
+ contentHash,
620
+ mimeType: contentType,
621
+ size,
622
+ filename: filename || null,
623
+ previousVersionId,
624
+ path: derivedPath,
639
625
  });
640
- const fileId = fileResult.rows[0].id;
641
626
  // Generate presigned PUT URL
642
627
  const uploadUrl = await generatePresignedPutUrl(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
643
628
  const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
@@ -0,0 +1,27 @@
1
+ import type { StorageModuleConfig } from './types';
2
+ interface RecordManagedFileInput {
3
+ bucketId: string;
4
+ key: string;
5
+ contentHash: string;
6
+ mimeType: string;
7
+ size: number;
8
+ filename: string | null | undefined;
9
+ previousVersionId?: string | null;
10
+ path?: string | null;
11
+ }
12
+ /**
13
+ * Record a managed file through the storage plane's generated recorder.
14
+ *
15
+ * The recorder owns the files-table insert so bucket inheritance and claims
16
+ * attribution run at the database boundary. Optional arguments are only named
17
+ * when the generated function supports them and the caller has a value.
18
+ */
19
+ export declare function recordManagedFile(pgClient: {
20
+ query: (opts: {
21
+ text: string;
22
+ values: unknown[];
23
+ }) => Promise<{
24
+ rows: unknown[];
25
+ }>;
26
+ }, storageConfig: StorageModuleConfig, input: RecordManagedFileInput): Promise<string>;
27
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Record a managed file through the storage plane's generated recorder.
3
+ *
4
+ * The recorder owns the files-table insert so bucket inheritance and claims
5
+ * attribution run at the database boundary. Optional arguments are only named
6
+ * when the generated function supports them and the caller has a value.
7
+ */
8
+ export async function recordManagedFile(pgClient, storageConfig, input) {
9
+ if (!storageConfig.recorderQualifiedName) {
10
+ throw new Error(`STORAGE_RECORDER_MISSING: storage module ${storageConfig.id} (${storageConfig.filesTableName}) ` +
11
+ `must expose ${storageConfig.filesTableName}_record_file in its private schema`);
12
+ }
13
+ if (input.previousVersionId != null && !storageConfig.hasVersioning) {
14
+ throw new Error(`STORAGE_VERSIONING_UNSUPPORTED: storage module ${storageConfig.id} (${storageConfig.filesTableName}) ` +
15
+ 'does not support previous_version_id');
16
+ }
17
+ if (input.path != null && !storageConfig.hasPathShares) {
18
+ throw new Error(`STORAGE_PATH_SHARES_UNSUPPORTED: storage module ${storageConfig.id} (${storageConfig.filesTableName}) ` +
19
+ 'does not support path');
20
+ }
21
+ const args = [
22
+ 'bucket_id := $1::uuid',
23
+ 'key := $2::text',
24
+ 'content_hash := $3::text',
25
+ 'mime_type := $4::text',
26
+ 'size := $5::bigint',
27
+ 'filename := $6::text',
28
+ 'upload := $7::jsonb',
29
+ ];
30
+ const values = [
31
+ input.bucketId,
32
+ input.key,
33
+ input.contentHash,
34
+ input.mimeType,
35
+ input.size,
36
+ input.filename ?? null,
37
+ null,
38
+ ];
39
+ if (storageConfig.hasVersioning && input.previousVersionId != null) {
40
+ args.push(`previous_version_id := $${values.length + 1}::uuid`);
41
+ values.push(input.previousVersionId);
42
+ }
43
+ if (storageConfig.hasPathShares && input.path != null) {
44
+ args.push(`path := $${values.length + 1}::text`);
45
+ values.push(input.path);
46
+ }
47
+ const result = await pgClient.query({
48
+ text: `SELECT id FROM ${storageConfig.recorderQualifiedName}(${args.join(', ')})`,
49
+ values,
50
+ });
51
+ return result.rows[0].id;
52
+ }
@@ -41,6 +41,7 @@ const ALL_STORAGE_MODULES_QUERY = `
41
41
  bt.name AS buckets_table,
42
42
  fs.schema_name AS files_schema,
43
43
  ft.name AS files_table,
44
+ ps.schema_name AS private_schema,
44
45
  sm.endpoint,
45
46
  sm.public_url_prefix,
46
47
  sm.provider,
@@ -53,6 +54,7 @@ const ALL_STORAGE_MODULES_QUERY = `
53
54
  sm.max_bulk_files,
54
55
  sm.max_bulk_total_size,
55
56
  sm.has_path_shares,
57
+ sm.has_versioning,
56
58
  sm.has_confirm_upload,
57
59
  es.schema_name AS entity_schema,
58
60
  et.name AS entity_table
@@ -61,6 +63,7 @@ const ALL_STORAGE_MODULES_QUERY = `
61
63
  JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
62
64
  JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
63
65
  JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
66
+ LEFT JOIN metaschema_public.schema ps ON ps.id = sm.private_schema_id
64
67
  LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
65
68
  LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
66
69
  WHERE sm.database_id = $1
@@ -74,6 +77,9 @@ function buildConfig(row) {
74
77
  id: row.id,
75
78
  bucketsQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.buckets_schema, row.buckets_table),
76
79
  filesQualifiedName: QuoteUtils.quoteQualifiedIdentifier(row.files_schema, row.files_table),
80
+ recorderQualifiedName: row.private_schema
81
+ ? QuoteUtils.quoteQualifiedIdentifier(row.private_schema, `${row.files_table}_record_file`)
82
+ : null,
77
83
  schemaName: row.buckets_schema,
78
84
  bucketsTableName: row.buckets_table,
79
85
  filesTableName: row.files_table,
@@ -92,6 +98,7 @@ function buildConfig(row) {
92
98
  maxFilenameLength: row.max_filename_length ?? DEFAULT_MAX_FILENAME_LENGTH,
93
99
  cacheTtlSeconds,
94
100
  hasPathShares: row.has_path_shares ?? false,
101
+ hasVersioning: row.has_versioning ?? false,
95
102
  hasConfirmUpload: row.has_confirm_upload ?? false,
96
103
  maxBulkFiles: row.max_bulk_files ?? DEFAULT_MAX_BULK_FILES,
97
104
  maxBulkTotalSize: row.max_bulk_total_size ?? DEFAULT_MAX_BULK_TOTAL_SIZE,
package/esm/types.d.ts CHANGED
@@ -35,6 +35,8 @@ export interface StorageModuleConfig {
35
35
  bucketsTableName: string;
36
36
  /** Files table name */
37
37
  filesTableName: string;
38
+ /** Qualified generated managed-file recorder, or null when unavailable */
39
+ recorderQualifiedName: string | null;
38
40
  /** Scope name (e.g., 'app', 'org', 'team') */
39
41
  scope: string;
40
42
  /** Entity table ID for entity-scoped storage (NULL for app-level) */
@@ -67,6 +69,8 @@ export interface StorageModuleConfig {
67
69
  * without it every row is treated as live, because there is nothing to read.
68
70
  */
69
71
  hasConfirmUpload: boolean;
72
+ /** Whether the files table carries the versioning chain. */
73
+ hasVersioning: boolean;
70
74
  /** Max files per requestBulkUploadUrls batch (default: 100) */
71
75
  maxBulkFiles: number;
72
76
  /** Max total size per bulk upload batch in bytes (default: 1GB) */
package/managed-upload.js CHANGED
@@ -30,6 +30,7 @@ const file_ref_registry_1 = require("./file-ref-registry");
30
30
  const physical_bucket_1 = require("./physical-bucket");
31
31
  const request_pg_client_1 = require("./request-pg-client");
32
32
  const s3_signer_1 = require("./s3-signer");
33
+ const storage_file_recorder_1 = require("./storage-file-recorder");
33
34
  const storage_module_cache_1 = require("./storage-module-cache");
34
35
  const log = new logger_1.Logger('graphile-presigned-url:managed-upload');
35
36
  /**
@@ -258,22 +259,14 @@ async function finalizeStagedUpload(args) {
258
259
  let fileId;
259
260
  try {
260
261
  fileId = await (0, request_pg_client_1.withRequestPgClient)(withPgClient, pgSettings, async (pgClient) => {
261
- const result = await pgClient.query({
262
- text: `INSERT INTO ${storageConfig.filesQualifiedName}
263
- (bucket_id, key, content_hash, mime_type, size, filename, is_public)
264
- VALUES ($1, $2, $3, $4, $5, $6, $7)
265
- RETURNING id`,
266
- values: [
267
- bucket.id,
268
- finalKey,
269
- staged.contentHash,
270
- staged.contentType,
271
- staged.size,
272
- staged.filename ?? null,
273
- bucket.is_public,
274
- ],
262
+ return (0, storage_file_recorder_1.recordManagedFile)(pgClient, storageConfig, {
263
+ bucketId: bucket.id,
264
+ key: finalKey,
265
+ contentHash: staged.contentHash,
266
+ mimeType: staged.contentType,
267
+ size: staged.size,
268
+ filename: staged.filename,
275
269
  });
276
- return result.rows[0].id;
277
270
  });
278
271
  }
279
272
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-presigned-url-plugin",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl mutation and downloadUrl computed field",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -44,7 +44,7 @@
44
44
  "@aws-sdk/s3-request-presigner": "^3.1052.0",
45
45
  "@pgpmjs/logger": "^2.25.0",
46
46
  "@pgsql/quotes": "^18.2.4",
47
- "graphile-storage-registry": "^0.1.0",
47
+ "graphile-storage-registry": "^0.1.1",
48
48
  "lru-cache": "^11.2.7",
49
49
  "mime-bytes": "^0.32.0"
50
50
  },
@@ -62,5 +62,5 @@
62
62
  "@types/node": "^22.19.11",
63
63
  "makage": "^0.3.0"
64
64
  },
65
- "gitHead": "df6b6613c844b505813e5d02d3a4afd918b74eab"
65
+ "gitHead": "b91ecc6db944b29a29b5474268de54ddca5dc34a"
66
66
  }
@@ -15,6 +15,7 @@ exports.mintPhysicalBucketName = mintPhysicalBucketName;
15
15
  exports.resolveS3ForDatabase = resolveS3ForDatabase;
16
16
  exports.provisionAndRecordPhysicalBucket = provisionAndRecordPhysicalBucket;
17
17
  const logger_1 = require("@pgpmjs/logger");
18
+ const graphile_storage_registry_1 = require("graphile-storage-registry");
18
19
  const request_pg_client_1 = require("./request-pg-client");
19
20
  const s3_failure_1 = require("./s3-failure");
20
21
  const storage_module_cache_1 = require("./storage-module-cache");
@@ -114,12 +115,7 @@ async function provisionAndRecordPhysicalBucket(options, withPgClient, storageCo
114
115
  // guard keeps this idempotent and race-safe across concurrent first uploads.
115
116
  // The catalog-sync trigger on this UPDATE needs `jwt.claims.database_id`, so the
116
117
  // write runs under the resolved database claim (privileged role preserved).
117
- await (0, request_pg_client_1.withRequestPgClient)(withPgClient, { 'jwt.claims.database_id': databaseId }, (client) => client.query({
118
- text: `UPDATE ${storageConfig.bucketsQualifiedName}
119
- SET physical_name = $1
120
- WHERE id = $2 AND physical_name IS NULL`,
121
- values: [s3BucketName, bucket.id],
122
- }));
118
+ await (0, request_pg_client_1.withRequestPgClient)(withPgClient, { 'jwt.claims.database_id': databaseId }, (client) => (0, graphile_storage_registry_1.recordPhysicalName)((query) => client.query(query), storageConfig.bucketsQualifiedName, bucket.id, s3BucketName));
123
119
  bucket.physical_name = s3BucketName;
124
120
  log.info(`Recorded physical_name="${s3BucketName}" on bucket ${bucket.id}`);
125
121
  return s3BucketName;
package/plugin.js CHANGED
@@ -34,6 +34,7 @@ const managed_upload_1 = require("./managed-upload");
34
34
  const physical_bucket_1 = require("./physical-bucket");
35
35
  const request_pg_client_1 = require("./request-pg-client");
36
36
  const s3_signer_1 = require("./s3-signer");
37
+ const storage_file_recorder_1 = require("./storage-file-recorder");
37
38
  const storage_module_cache_1 = require("./storage-module-cache");
38
39
  const log = new logger_1.Logger('graphile-presigned-url:plugin');
39
40
  // --- Protocol-level constants (not configurable) ---
@@ -616,32 +617,16 @@ async function processSingleFile(options, txClient, storageConfig, databaseId, b
616
617
  }
617
618
  // Auto-derive ltree path from custom key directory (only when has_path_shares)
618
619
  const derivedPath = isCustomKey && storageConfig.hasPathShares ? derivePathFromKey(s3Key) : null;
619
- // Create file record. An entity-keyed plane (the module records an entity
620
- // table) carries owner_id on its rows; app- and database-scope planes do not.
621
- const hasOwnerColumn = storageConfig.entityTableId !== null;
622
- const columns = ['bucket_id', 'key', 'content_hash', 'mime_type', 'size', 'filename', 'is_public'];
623
- const values = [bucket.id, s3Key, contentHash, contentType, size, filename || null, bucket.is_public];
624
- if (hasOwnerColumn) {
625
- columns.push('owner_id');
626
- values.push(bucket.owner_id);
627
- }
628
- if (previousVersionId) {
629
- columns.push('previous_version_id');
630
- values.push(previousVersionId);
631
- }
632
- if (derivedPath) {
633
- columns.push('path');
634
- values.push(derivedPath);
635
- }
636
- const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
637
- const fileResult = await txClient.query({
638
- text: `INSERT INTO ${storageConfig.filesQualifiedName}
639
- (${columns.join(', ')})
640
- VALUES (${placeholders})
641
- RETURNING id`,
642
- values,
620
+ const fileId = await (0, storage_file_recorder_1.recordManagedFile)(txClient, storageConfig, {
621
+ bucketId: bucket.id,
622
+ key: s3Key,
623
+ contentHash,
624
+ mimeType: contentType,
625
+ size,
626
+ filename: filename || null,
627
+ previousVersionId,
628
+ path: derivedPath,
643
629
  });
644
- const fileId = fileResult.rows[0].id;
645
630
  // Generate presigned PUT URL
646
631
  const uploadUrl = await (0, s3_signer_1.generatePresignedPutUrl)(s3ForDb, s3Key, contentType, size, storageConfig.uploadUrlExpirySeconds);
647
632
  const expiresAt = new Date(Date.now() + storageConfig.uploadUrlExpirySeconds * 1000).toISOString();
@@ -0,0 +1,27 @@
1
+ import type { StorageModuleConfig } from './types';
2
+ interface RecordManagedFileInput {
3
+ bucketId: string;
4
+ key: string;
5
+ contentHash: string;
6
+ mimeType: string;
7
+ size: number;
8
+ filename: string | null | undefined;
9
+ previousVersionId?: string | null;
10
+ path?: string | null;
11
+ }
12
+ /**
13
+ * Record a managed file through the storage plane's generated recorder.
14
+ *
15
+ * The recorder owns the files-table insert so bucket inheritance and claims
16
+ * attribution run at the database boundary. Optional arguments are only named
17
+ * when the generated function supports them and the caller has a value.
18
+ */
19
+ export declare function recordManagedFile(pgClient: {
20
+ query: (opts: {
21
+ text: string;
22
+ values: unknown[];
23
+ }) => Promise<{
24
+ rows: unknown[];
25
+ }>;
26
+ }, storageConfig: StorageModuleConfig, input: RecordManagedFileInput): Promise<string>;
27
+ export {};
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordManagedFile = recordManagedFile;
4
+ /**
5
+ * Record a managed file through the storage plane's generated recorder.
6
+ *
7
+ * The recorder owns the files-table insert so bucket inheritance and claims
8
+ * attribution run at the database boundary. Optional arguments are only named
9
+ * when the generated function supports them and the caller has a value.
10
+ */
11
+ async function recordManagedFile(pgClient, storageConfig, input) {
12
+ if (!storageConfig.recorderQualifiedName) {
13
+ throw new Error(`STORAGE_RECORDER_MISSING: storage module ${storageConfig.id} (${storageConfig.filesTableName}) ` +
14
+ `must expose ${storageConfig.filesTableName}_record_file in its private schema`);
15
+ }
16
+ if (input.previousVersionId != null && !storageConfig.hasVersioning) {
17
+ throw new Error(`STORAGE_VERSIONING_UNSUPPORTED: storage module ${storageConfig.id} (${storageConfig.filesTableName}) ` +
18
+ 'does not support previous_version_id');
19
+ }
20
+ if (input.path != null && !storageConfig.hasPathShares) {
21
+ throw new Error(`STORAGE_PATH_SHARES_UNSUPPORTED: storage module ${storageConfig.id} (${storageConfig.filesTableName}) ` +
22
+ 'does not support path');
23
+ }
24
+ const args = [
25
+ 'bucket_id := $1::uuid',
26
+ 'key := $2::text',
27
+ 'content_hash := $3::text',
28
+ 'mime_type := $4::text',
29
+ 'size := $5::bigint',
30
+ 'filename := $6::text',
31
+ 'upload := $7::jsonb',
32
+ ];
33
+ const values = [
34
+ input.bucketId,
35
+ input.key,
36
+ input.contentHash,
37
+ input.mimeType,
38
+ input.size,
39
+ input.filename ?? null,
40
+ null,
41
+ ];
42
+ if (storageConfig.hasVersioning && input.previousVersionId != null) {
43
+ args.push(`previous_version_id := $${values.length + 1}::uuid`);
44
+ values.push(input.previousVersionId);
45
+ }
46
+ if (storageConfig.hasPathShares && input.path != null) {
47
+ args.push(`path := $${values.length + 1}::text`);
48
+ values.push(input.path);
49
+ }
50
+ const result = await pgClient.query({
51
+ text: `SELECT id FROM ${storageConfig.recorderQualifiedName}(${args.join(', ')})`,
52
+ values,
53
+ });
54
+ return result.rows[0].id;
55
+ }
@@ -52,6 +52,7 @@ const ALL_STORAGE_MODULES_QUERY = `
52
52
  bt.name AS buckets_table,
53
53
  fs.schema_name AS files_schema,
54
54
  ft.name AS files_table,
55
+ ps.schema_name AS private_schema,
55
56
  sm.endpoint,
56
57
  sm.public_url_prefix,
57
58
  sm.provider,
@@ -64,6 +65,7 @@ const ALL_STORAGE_MODULES_QUERY = `
64
65
  sm.max_bulk_files,
65
66
  sm.max_bulk_total_size,
66
67
  sm.has_path_shares,
68
+ sm.has_versioning,
67
69
  sm.has_confirm_upload,
68
70
  es.schema_name AS entity_schema,
69
71
  et.name AS entity_table
@@ -72,6 +74,7 @@ const ALL_STORAGE_MODULES_QUERY = `
72
74
  JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
73
75
  JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
74
76
  JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
77
+ LEFT JOIN metaschema_public.schema ps ON ps.id = sm.private_schema_id
75
78
  LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
76
79
  LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
77
80
  WHERE sm.database_id = $1
@@ -85,6 +88,9 @@ function buildConfig(row) {
85
88
  id: row.id,
86
89
  bucketsQualifiedName: quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.buckets_schema, row.buckets_table),
87
90
  filesQualifiedName: quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.files_schema, row.files_table),
91
+ recorderQualifiedName: row.private_schema
92
+ ? quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.private_schema, `${row.files_table}_record_file`)
93
+ : null,
88
94
  schemaName: row.buckets_schema,
89
95
  bucketsTableName: row.buckets_table,
90
96
  filesTableName: row.files_table,
@@ -103,6 +109,7 @@ function buildConfig(row) {
103
109
  maxFilenameLength: row.max_filename_length ?? DEFAULT_MAX_FILENAME_LENGTH,
104
110
  cacheTtlSeconds,
105
111
  hasPathShares: row.has_path_shares ?? false,
112
+ hasVersioning: row.has_versioning ?? false,
106
113
  hasConfirmUpload: row.has_confirm_upload ?? false,
107
114
  maxBulkFiles: row.max_bulk_files ?? DEFAULT_MAX_BULK_FILES,
108
115
  maxBulkTotalSize: row.max_bulk_total_size ?? DEFAULT_MAX_BULK_TOTAL_SIZE,
package/types.d.ts CHANGED
@@ -35,6 +35,8 @@ export interface StorageModuleConfig {
35
35
  bucketsTableName: string;
36
36
  /** Files table name */
37
37
  filesTableName: string;
38
+ /** Qualified generated managed-file recorder, or null when unavailable */
39
+ recorderQualifiedName: string | null;
38
40
  /** Scope name (e.g., 'app', 'org', 'team') */
39
41
  scope: string;
40
42
  /** Entity table ID for entity-scoped storage (NULL for app-level) */
@@ -67,6 +69,8 @@ export interface StorageModuleConfig {
67
69
  * without it every row is treated as live, because there is nothing to read.
68
70
  */
69
71
  hasConfirmUpload: boolean;
72
+ /** Whether the files table carries the versioning chain. */
73
+ hasVersioning: boolean;
70
74
  /** Max files per requestBulkUploadUrls batch (default: 100) */
71
75
  maxBulkFiles: number;
72
76
  /** Max total size per bulk upload batch in bytes (default: 1GB) */