graphile-presigned-url-plugin 1.16.1 → 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.
- package/esm/managed-upload.js +8 -15
- package/esm/plugin.js +10 -25
- package/esm/storage-file-recorder.d.ts +27 -0
- package/esm/storage-file-recorder.js +52 -0
- package/esm/storage-module-cache.js +7 -0
- package/esm/types.d.ts +4 -0
- package/managed-upload.js +8 -15
- package/package.json +2 -2
- package/plugin.js +10 -25
- package/storage-file-recorder.d.ts +27 -0
- package/storage-file-recorder.js +55 -0
- package/storage-module-cache.js +7 -0
- package/types.d.ts +4 -0
package/esm/managed-upload.js
CHANGED
|
@@ -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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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) {
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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.
|
|
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",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"@types/node": "^22.19.11",
|
|
63
63
|
"makage": "^0.3.0"
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "b91ecc6db944b29a29b5474268de54ddca5dc34a"
|
|
66
66
|
}
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
+
}
|
package/storage-module-cache.js
CHANGED
|
@@ -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) */
|