graphile-presigned-url-plugin 0.9.0 → 0.11.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.
@@ -28,9 +28,16 @@ export declare function generatePresignedPutUrl(s3Config: S3Config, key: string,
28
28
  */
29
29
  export declare function generatePresignedGetUrl(s3Config: S3Config, key: string, expiresIn?: number, filename?: string): Promise<string>;
30
30
  /**
31
- * Check if an object exists in S3 and optionally verify its content-type.
31
+ * Delete an object from S3.
32
+ *
33
+ * Idempotent — deleting a non-existent key is a no-op (S3 returns 204).
32
34
  *
33
- * Checks whether an object exists in S3 and retrieves its content-type.
35
+ * @param s3Config - S3 client and bucket configuration
36
+ * @param key - S3 object key to delete
37
+ */
38
+ export declare function deleteS3Object(s3Config: S3Config, key: string): Promise<void>;
39
+ /**
40
+ * Check if an object exists in S3 and optionally verify its content-type.
34
41
  *
35
42
  * @param s3Config - S3 client and bucket configuration
36
43
  * @param key - S3 object key
package/esm/s3-signer.js CHANGED
@@ -1,4 +1,4 @@
1
- import { PutObjectCommand, GetObjectCommand, HeadObjectCommand, } from '@aws-sdk/client-s3';
1
+ import { PutObjectCommand, GetObjectCommand, HeadObjectCommand, DeleteObjectCommand, } from '@aws-sdk/client-s3';
2
2
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
3
  import { Logger } from '@pgpmjs/logger';
4
4
  const log = new Logger('graphile-presigned-url:s3');
@@ -54,9 +54,22 @@ export async function generatePresignedGetUrl(s3Config, key, expiresIn = 3600, f
54
54
  return url;
55
55
  }
56
56
  /**
57
- * Check if an object exists in S3 and optionally verify its content-type.
57
+ * Delete an object from S3.
58
+ *
59
+ * Idempotent — deleting a non-existent key is a no-op (S3 returns 204).
58
60
  *
59
- * Checks whether an object exists in S3 and retrieves its content-type.
61
+ * @param s3Config - S3 client and bucket configuration
62
+ * @param key - S3 object key to delete
63
+ */
64
+ export async function deleteS3Object(s3Config, key) {
65
+ await s3Config.client.send(new DeleteObjectCommand({
66
+ Bucket: s3Config.bucket,
67
+ Key: key,
68
+ }));
69
+ log.debug(`Deleted S3 object: bucket=${s3Config.bucket}, key=${key}`);
70
+ }
71
+ /**
72
+ * Check if an object exists in S3 and optionally verify its content-type.
60
73
  *
61
74
  * @param s3Config - S3 client and bucket configuration
62
75
  * @param key - S3 object key
@@ -66,6 +66,39 @@ export declare function resolveStorageModuleByFileId(pgClient: {
66
66
  bucket_id: string;
67
67
  };
68
68
  } | null>;
69
+ /**
70
+ * Load all storage modules for a database, using the LRU cache.
71
+ *
72
+ * Returns an array of all StorageModuleConfig entries (app-level + entity-scoped).
73
+ * The result is cached per-database so subsequent calls avoid the DB query.
74
+ */
75
+ export declare function loadAllStorageModules(pgClient: {
76
+ query: (opts: {
77
+ text: string;
78
+ values?: unknown[];
79
+ }) => Promise<{
80
+ rows: unknown[];
81
+ }>;
82
+ }, databaseId: string): Promise<StorageModuleConfig[]>;
83
+ /**
84
+ * Resolve the storage module config from a PostGraphile pgCodec.
85
+ *
86
+ * Matches the codec's schema + table name against cached storage modules.
87
+ * Works for both files codecs (@storageFiles) and buckets codecs (@storageBuckets).
88
+ *
89
+ * @param pgCodec - The PostGraphile codec (has extensions.pg.schemaName, name)
90
+ * @param allConfigs - All storage module configs for this database
91
+ * @returns The matching StorageModuleConfig or null
92
+ */
93
+ export declare function resolveStorageConfigFromCodec(pgCodec: {
94
+ name: string;
95
+ extensions?: {
96
+ pg?: {
97
+ schemaName?: string;
98
+ };
99
+ };
100
+ sqlType?: string;
101
+ }, allConfigs: StorageModuleConfig[]): StorageModuleConfig | null;
69
102
  /**
70
103
  * Resolve bucket metadata for a given database + bucket key, using the LRU cache.
71
104
  *
@@ -257,6 +257,51 @@ export async function resolveStorageModuleByFileId(pgClient, databaseId, fileId)
257
257
  }
258
258
  return null;
259
259
  }
260
+ /**
261
+ * Load all storage modules for a database, using the LRU cache.
262
+ *
263
+ * Returns an array of all StorageModuleConfig entries (app-level + entity-scoped).
264
+ * The result is cached per-database so subsequent calls avoid the DB query.
265
+ */
266
+ export async function loadAllStorageModules(pgClient, databaseId) {
267
+ const cacheKey = `storage:${databaseId}:all-list`;
268
+ const cached = storageModuleCache.get(cacheKey);
269
+ if (cached) {
270
+ return cached._allConfigs;
271
+ }
272
+ log.debug(`Loading all storage modules for database ${databaseId}`);
273
+ const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] });
274
+ const configs = result.rows.map(buildConfig);
275
+ // Cache each individual config by its membership type
276
+ for (const config of configs) {
277
+ const key = config.membershipType === null
278
+ ? `storage:${databaseId}:app`
279
+ : `storage:${databaseId}:mt:${config.membershipType}`;
280
+ storageModuleCache.set(key, config);
281
+ }
282
+ // Store the full list under a sentinel key
283
+ const sentinel = { ...configs[0] || {}, _allConfigs: configs };
284
+ storageModuleCache.set(cacheKey, sentinel);
285
+ return configs;
286
+ }
287
+ /**
288
+ * Resolve the storage module config from a PostGraphile pgCodec.
289
+ *
290
+ * Matches the codec's schema + table name against cached storage modules.
291
+ * Works for both files codecs (@storageFiles) and buckets codecs (@storageBuckets).
292
+ *
293
+ * @param pgCodec - The PostGraphile codec (has extensions.pg.schemaName, name)
294
+ * @param allConfigs - All storage module configs for this database
295
+ * @returns The matching StorageModuleConfig or null
296
+ */
297
+ export function resolveStorageConfigFromCodec(pgCodec, allConfigs) {
298
+ const schemaName = pgCodec.extensions?.pg?.schemaName;
299
+ const tableName = pgCodec.name;
300
+ if (!schemaName || !tableName)
301
+ return null;
302
+ return allConfigs.find((c) => (c.filesTableName === tableName && c.schemaName === schemaName) ||
303
+ (c.bucketsTableName === tableName && c.schemaName === schemaName)) || null;
304
+ }
260
305
  // --- Bucket metadata cache ---
261
306
  /**
262
307
  * LRU cache for per-database bucket metadata.
package/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Presigned URL Plugin for PostGraphile v5
3
3
  *
4
- * Provides presigned URL upload capabilities for PostGraphile v5:
5
- * - requestUploadUrl mutation (presigned PUT URL generation + dedup)
6
- * - downloadUrl computed field (presigned GET URL / public URL)
4
+ * Provides per-table S3 storage middleware for PostGraphile v5:
5
+ * - Upload fields on @storageBuckets types (requestUploadUrl, requestBulkUploadUrls)
6
+ * - Delete middleware on @storageFiles tables (S3 cleanup on delete)
7
+ * - downloadUrl computed field on @storageFiles types
7
8
  *
8
9
  * @example
9
10
  * ```typescript
@@ -28,6 +29,6 @@
28
29
  export { PresignedUrlPlugin, createPresignedUrlPlugin } from './plugin';
29
30
  export { createDownloadUrlPlugin } from './download-url-field';
30
31
  export { PresignedUrlPreset } from './preset';
31
- export { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, resolveStorageModuleByFileId, clearStorageModuleCache, clearBucketCache, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
32
- export { generatePresignedPutUrl, generatePresignedGetUrl, headObject } from './s3-signer';
32
+ export { getStorageModuleConfig, getStorageModuleConfigForOwner, getBucketConfig, resolveStorageModuleByFileId, loadAllStorageModules, resolveStorageConfigFromCodec, clearStorageModuleCache, clearBucketCache, isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
33
+ export { generatePresignedPutUrl, generatePresignedGetUrl, deleteS3Object, headObject } from './s3-signer';
33
34
  export type { BucketConfig, StorageModuleConfig, RequestUploadUrlInput, RequestUploadUrlPayload, S3Config, S3ConfigOrGetter, PresignedUrlPluginOptions, BucketNameResolver, EnsureBucketProvisioned, } from './types';
package/index.js CHANGED
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * Presigned URL Plugin for PostGraphile v5
4
4
  *
5
- * Provides presigned URL upload capabilities for PostGraphile v5:
6
- * - requestUploadUrl mutation (presigned PUT URL generation + dedup)
7
- * - downloadUrl computed field (presigned GET URL / public URL)
5
+ * Provides per-table S3 storage middleware for PostGraphile v5:
6
+ * - Upload fields on @storageBuckets types (requestUploadUrl, requestBulkUploadUrls)
7
+ * - Delete middleware on @storageFiles tables (S3 cleanup on delete)
8
+ * - downloadUrl computed field on @storageFiles types
8
9
  *
9
10
  * @example
10
11
  * ```typescript
@@ -27,7 +28,7 @@
27
28
  * ```
28
29
  */
29
30
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.headObject = exports.generatePresignedGetUrl = exports.generatePresignedPutUrl = exports.markS3BucketProvisioned = exports.isS3BucketProvisioned = exports.clearBucketCache = exports.clearStorageModuleCache = exports.resolveStorageModuleByFileId = exports.getBucketConfig = exports.getStorageModuleConfigForOwner = exports.getStorageModuleConfig = exports.PresignedUrlPreset = exports.createDownloadUrlPlugin = exports.createPresignedUrlPlugin = exports.PresignedUrlPlugin = void 0;
31
+ exports.headObject = exports.deleteS3Object = exports.generatePresignedGetUrl = exports.generatePresignedPutUrl = exports.markS3BucketProvisioned = exports.isS3BucketProvisioned = exports.clearBucketCache = exports.clearStorageModuleCache = exports.resolveStorageConfigFromCodec = exports.loadAllStorageModules = exports.resolveStorageModuleByFileId = exports.getBucketConfig = exports.getStorageModuleConfigForOwner = exports.getStorageModuleConfig = exports.PresignedUrlPreset = exports.createDownloadUrlPlugin = exports.createPresignedUrlPlugin = exports.PresignedUrlPlugin = void 0;
31
32
  var plugin_1 = require("./plugin");
32
33
  Object.defineProperty(exports, "PresignedUrlPlugin", { enumerable: true, get: function () { return plugin_1.PresignedUrlPlugin; } });
33
34
  Object.defineProperty(exports, "createPresignedUrlPlugin", { enumerable: true, get: function () { return plugin_1.createPresignedUrlPlugin; } });
@@ -40,6 +41,8 @@ Object.defineProperty(exports, "getStorageModuleConfig", { enumerable: true, get
40
41
  Object.defineProperty(exports, "getStorageModuleConfigForOwner", { enumerable: true, get: function () { return storage_module_cache_1.getStorageModuleConfigForOwner; } });
41
42
  Object.defineProperty(exports, "getBucketConfig", { enumerable: true, get: function () { return storage_module_cache_1.getBucketConfig; } });
42
43
  Object.defineProperty(exports, "resolveStorageModuleByFileId", { enumerable: true, get: function () { return storage_module_cache_1.resolveStorageModuleByFileId; } });
44
+ Object.defineProperty(exports, "loadAllStorageModules", { enumerable: true, get: function () { return storage_module_cache_1.loadAllStorageModules; } });
45
+ Object.defineProperty(exports, "resolveStorageConfigFromCodec", { enumerable: true, get: function () { return storage_module_cache_1.resolveStorageConfigFromCodec; } });
43
46
  Object.defineProperty(exports, "clearStorageModuleCache", { enumerable: true, get: function () { return storage_module_cache_1.clearStorageModuleCache; } });
44
47
  Object.defineProperty(exports, "clearBucketCache", { enumerable: true, get: function () { return storage_module_cache_1.clearBucketCache; } });
45
48
  Object.defineProperty(exports, "isS3BucketProvisioned", { enumerable: true, get: function () { return storage_module_cache_1.isS3BucketProvisioned; } });
@@ -47,4 +50,5 @@ Object.defineProperty(exports, "markS3BucketProvisioned", { enumerable: true, ge
47
50
  var s3_signer_1 = require("./s3-signer");
48
51
  Object.defineProperty(exports, "generatePresignedPutUrl", { enumerable: true, get: function () { return s3_signer_1.generatePresignedPutUrl; } });
49
52
  Object.defineProperty(exports, "generatePresignedGetUrl", { enumerable: true, get: function () { return s3_signer_1.generatePresignedGetUrl; } });
53
+ Object.defineProperty(exports, "deleteS3Object", { enumerable: true, get: function () { return s3_signer_1.deleteS3Object; } });
50
54
  Object.defineProperty(exports, "headObject", { enumerable: true, get: function () { return s3_signer_1.headObject; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-presigned-url-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@aws-sdk/client-s3": "^3.1009.0",
44
44
  "@aws-sdk/s3-request-presigner": "^3.1009.0",
45
- "@pgpmjs/logger": "^2.6.0",
45
+ "@pgpmjs/logger": "^2.7.0",
46
46
  "@pgsql/quotes": "^17.1.0",
47
47
  "lru-cache": "^11.2.7"
48
48
  },
@@ -56,9 +56,9 @@
56
56
  "postgraphile": "5.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@constructive-io/s3-utils": "^2.12.1",
59
+ "@constructive-io/s3-utils": "^2.13.0",
60
60
  "@types/node": "^22.19.11",
61
61
  "makage": "^0.1.10"
62
62
  },
63
- "gitHead": "caf38236170287e2ad06d5c47482ce246fb3233e"
63
+ "gitHead": "7409479a981ff63a0937b5406a1c206a07b264ad"
64
64
  }
package/plugin.d.ts CHANGED
@@ -1,18 +1,22 @@
1
1
  /**
2
- * Presigned URL Plugin for PostGraphile v5
2
+ * Per-Table Storage Middleware Plugin for PostGraphile v5
3
3
  *
4
- * Adds presigned URL upload support to PostGraphile v5:
4
+ * Hooks into PostGraphile's auto-generated CRUD mutations to add S3 operations:
5
5
  *
6
- * 1. `requestUploadUrl` mutationgenerates a presigned PUT URL for direct
7
- * client-to-S3 upload. Checks bucket access via RLS, deduplicates by
8
- * content hash via UNIQUE(bucket_id, key) constraint.
6
+ * 1. Delete middlewarewraps `delete*` mutations on `@storageFiles`-tagged tables
7
+ * with S3 object cleanup (sync + async GC fallback via AFTER DELETE trigger).
9
8
  *
10
- * 2. `downloadUrl` computed field on File types generates presigned GET URLs
11
- * for private files, returns public URL prefix + key for public files.
9
+ * 2. Upload fields adds `requestUploadUrl` and `requestBulkUploadUrls` fields
10
+ * on `@storageBuckets`-tagged types, so clients upload via the typed bucket API.
12
11
  *
13
- * Uses the extendSchema + grafast plan pattern (same as PublicKeySignature).
12
+ * 3. downloadUrl handled by download-url-field.ts (separate plugin).
13
+ *
14
+ * No global mutations — all S3 operations are scoped to the per-table types that
15
+ * PostGraphile already generates. Scope resolution uses the codec's schema/table
16
+ * name matched against cached storage module configs.
14
17
  */
15
18
  import type { GraphileConfig } from 'graphile-config';
19
+ import 'graphile-build';
16
20
  import type { PresignedUrlPluginOptions } from './types';
17
21
  export declare function createPresignedUrlPlugin(options: PresignedUrlPluginOptions): GraphileConfig.Plugin;
18
22
  export declare const PresignedUrlPlugin: typeof createPresignedUrlPlugin;