graphile-presigned-url-plugin 0.19.2 β†’ 0.20.1

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/README.md CHANGED
@@ -92,6 +92,21 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
92
92
  * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **πŸ“ Type definitions** for PostgreSQL AST nodes in TypeScript.
93
93
  * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **πŸ› οΈ AST utilities** for constructing and transforming PostgreSQL syntax trees.
94
94
 
95
+ ### πŸ“š Documentation & Skills
96
+
97
+ * [constructive-skills](https://github.com/constructive-io/constructive-skills): **πŸ“– Platform documentation and AI agent skills** β€” feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
98
+
99
+ Install skills for AI coding agents:
100
+
101
+ ```bash
102
+ # All platform skills (security, blueprints, codegen, billing, etc.)
103
+ npx skills add constructive-io/constructive-skills
104
+
105
+ # Individual repo skills (pgpm, testing, CLI, search, etc.)
106
+ npx skills add https://github.com/constructive-io/constructive --skill pgpm
107
+ npx skills add https://github.com/constructive-io/constructive --skill constructive-testing
108
+ ```
109
+
95
110
  ## Credits
96
111
 
97
112
  **πŸ›  Built by the [Constructive](https://constructive.io) team β€” creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
@@ -115,7 +115,7 @@ function createDownloadUrlPlugin(options) {
115
115
  let downloadUrlExpirySeconds = 3600;
116
116
  try {
117
117
  if (withPgClient && pgSettings) {
118
- const resolved = await withPgClient(null, async (pgClient) => {
118
+ const resolved = await withPgClient(pgSettings, async (pgClient) => {
119
119
  const dbResult = await pgClient.query({
120
120
  text: `SELECT jwt_private.current_database_id() AS id`,
121
121
  });
@@ -112,7 +112,7 @@ export function createDownloadUrlPlugin(options) {
112
112
  let downloadUrlExpirySeconds = 3600;
113
113
  try {
114
114
  if (withPgClient && pgSettings) {
115
- const resolved = await withPgClient(null, async (pgClient) => {
115
+ const resolved = await withPgClient(pgSettings, async (pgClient) => {
116
116
  const dbResult = await pgClient.query({
117
117
  text: `SELECT jwt_private.current_database_id() AS id`,
118
118
  });
package/esm/plugin.js CHANGED
@@ -559,7 +559,7 @@ async function processSingleFile(options, txClient, storageConfig, databaseId, b
559
559
  // Auto-derive ltree path from custom key directory (only when has_path_shares)
560
560
  const derivedPath = isCustomKey && storageConfig.hasPathShares ? derivePathFromKey(s3Key) : null;
561
561
  // Create file record
562
- const hasOwnerColumn = storageConfig.membershipType !== null;
562
+ const hasOwnerColumn = storageConfig.scope !== 'app';
563
563
  const columns = ['bucket_id', 'key', 'content_hash', 'mime_type', 'size', 'filename', 'is_public'];
564
564
  const values = [bucket.id, s3Key, contentHash, contentType, size, filename || null, bucket.is_public];
565
565
  if (hasOwnerColumn) {
@@ -3,7 +3,7 @@ import type { StorageModuleConfig, BucketConfig } from './types';
3
3
  * Resolve the app-level storage module config for a database, using the LRU cache.
4
4
  *
5
5
  * This is the default path when no ownerId is provided. It returns the
6
- * storage module with membership_type IS NULL (app-level / database-wide).
6
+ * storage module with scope = 'app' (app-level / database-wide).
7
7
  *
8
8
  * @param pgClient - A pg client from the Graphile context (withPgClient or pgClient)
9
9
  * @param databaseId - The metaschema database UUID
@@ -31,14 +31,14 @@ const storageModuleCache = new LRUCache({
31
31
  * SQL query to resolve the app-level storage module config for a database.
32
32
  *
33
33
  * Joins storage_module β†’ table β†’ schema to get fully-qualified table names.
34
- * Filters to app-level (membership_type IS NULL) by default.
34
+ * Filters to app-level (scope = 'app') by default.
35
35
  *
36
- * Requires the multi-scope schema (membership_type column on storage_module).
36
+ * Requires the multi-scope schema (scope column on storage_module).
37
37
  */
38
38
  const APP_STORAGE_MODULE_QUERY = `
39
39
  SELECT
40
40
  sm.id,
41
- sm.membership_type,
41
+ sm.scope,
42
42
  sm.entity_table_id,
43
43
  bs.schema_name AS buckets_schema,
44
44
  bt.name AS buckets_table,
@@ -64,7 +64,7 @@ const APP_STORAGE_MODULE_QUERY = `
64
64
  JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
65
65
  JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
66
66
  WHERE sm.database_id = $1
67
- AND sm.membership_type IS NULL
67
+ AND sm.scope = 'app'
68
68
  LIMIT 1
69
69
  `;
70
70
  /**
@@ -76,7 +76,7 @@ const APP_STORAGE_MODULE_QUERY = `
76
76
  const ALL_STORAGE_MODULES_QUERY = `
77
77
  SELECT
78
78
  sm.id,
79
- sm.membership_type,
79
+ sm.scope,
80
80
  sm.entity_table_id,
81
81
  bs.schema_name AS buckets_schema,
82
82
  bt.name AS buckets_table,
@@ -117,7 +117,7 @@ function buildConfig(row) {
117
117
  schemaName: row.buckets_schema,
118
118
  bucketsTableName: row.buckets_table,
119
119
  filesTableName: row.files_table,
120
- membershipType: row.membership_type,
120
+ scope: row.scope,
121
121
  entityTableId: row.entity_table_id,
122
122
  entityQualifiedName: row.entity_schema && row.entity_table
123
123
  ? QuoteUtils.quoteQualifiedIdentifier(row.entity_schema, row.entity_table)
@@ -140,7 +140,7 @@ function buildConfig(row) {
140
140
  * Resolve the app-level storage module config for a database, using the LRU cache.
141
141
  *
142
142
  * This is the default path when no ownerId is provided. It returns the
143
- * storage module with membership_type IS NULL (app-level / database-wide).
143
+ * storage module with scope = 'app' (app-level / database-wide).
144
144
  *
145
145
  * @param pgClient - A pg client from the Graphile context (withPgClient or pgClient)
146
146
  * @param databaseId - The metaschema database UUID
@@ -201,11 +201,9 @@ export async function getStorageModuleConfigForOwner(pgClient, databaseId, owner
201
201
  log.debug(`Loading all storage modules for database ${databaseId} to resolve ownerId ${ownerId}`);
202
202
  const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] });
203
203
  allConfigs = result.rows.map(buildConfig);
204
- // Cache each individual config by its membership type
204
+ // Cache each individual config by its scope
205
205
  for (const config of allConfigs) {
206
- const key = config.membershipType === null
207
- ? `storage:${databaseId}:app`
208
- : `storage:${databaseId}:mt:${config.membershipType}`;
206
+ const key = `storage:${databaseId}:scope:${config.scope}`;
209
207
  storageModuleCache.set(key, config);
210
208
  }
211
209
  }
@@ -220,7 +218,7 @@ export async function getStorageModuleConfigForOwner(pgClient, databaseId, owner
220
218
  // Found the matching module β€” cache the ownerIdβ†’module mapping
221
219
  storageModuleCache.set(ownerCacheKey, mod);
222
220
  log.debug(`Resolved ownerId ${ownerId} to storage module ${mod.id} ` +
223
- `(membershipType=${mod.membershipType}, table=${mod.bucketsQualifiedName})`);
221
+ `(scope=${mod.scope}, table=${mod.bucketsQualifiedName})`);
224
222
  return mod;
225
223
  }
226
224
  }
@@ -272,16 +270,16 @@ export async function loadAllStorageModules(pgClient, databaseId) {
272
270
  log.debug(`Loading all storage modules for database ${databaseId}`);
273
271
  const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] });
274
272
  const configs = result.rows.map(buildConfig);
275
- // Cache each individual config by its membership type
273
+ // Cache each individual config by its scope
276
274
  for (const config of configs) {
277
- const key = config.membershipType === null
278
- ? `storage:${databaseId}:app`
279
- : `storage:${databaseId}:mt:${config.membershipType}`;
275
+ const key = `storage:${databaseId}:scope:${config.scope}`;
280
276
  storageModuleCache.set(key, config);
281
277
  }
282
- // Store the full list under a sentinel key
283
- const sentinel = { ...configs[0] || {}, _allConfigs: configs };
284
- storageModuleCache.set(cacheKey, sentinel);
278
+ // Store the full list under a sentinel key (only if non-empty to avoid caching failed lookups)
279
+ if (configs.length > 0) {
280
+ const sentinel = { ...configs[0], _allConfigs: configs };
281
+ storageModuleCache.set(cacheKey, sentinel);
282
+ }
285
283
  return configs;
286
284
  }
287
285
  /**
@@ -344,14 +342,15 @@ export async function getBucketConfig(pgClient, storageConfig, databaseId, bucke
344
342
  log.debug(`Bucket cache miss for ${databaseId}:${bucketKey}${ownerId ? ` (owner=${ownerId})` : ''}, querying DB...`);
345
343
  // Entity-scoped buckets use (owner_id, key) composite lookup;
346
344
  // app-level buckets just use key.
347
- const hasOwner = ownerId && storageConfig.membershipType !== null;
345
+ const isEntityScoped = storageConfig.scope !== 'app';
346
+ const hasOwner = ownerId && isEntityScoped;
348
347
  const result = await pgClient.query({
349
348
  text: hasOwner
350
349
  ? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size, allow_custom_keys
351
350
  FROM ${storageConfig.bucketsQualifiedName}
352
351
  WHERE key = $1 AND owner_id = $2
353
352
  LIMIT 1`
354
- : `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys
353
+ : `SELECT id, key, type, is_public, ${isEntityScoped ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys
355
354
  FROM ${storageConfig.bucketsQualifiedName}
356
355
  WHERE key = $1
357
356
  LIMIT 1`,
@@ -372,7 +371,7 @@ export async function getBucketConfig(pgClient, storageConfig, databaseId, bucke
372
371
  allow_custom_keys: row.allow_custom_keys ?? false,
373
372
  };
374
373
  bucketCache.set(cacheKey, config);
375
- log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.membershipType ?? 'app'})`);
374
+ log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.scope})`);
376
375
  return config;
377
376
  }
378
377
  // --- S3 bucket existence cache ---
package/esm/types.d.ts CHANGED
@@ -28,8 +28,8 @@ export interface StorageModuleConfig {
28
28
  bucketsTableName: string;
29
29
  /** Files table name */
30
30
  filesTableName: string;
31
- /** Membership type (NULL for app-level, non-NULL for entity-scoped) */
32
- membershipType: number | null;
31
+ /** Scope name (e.g., 'app', 'org', 'team') */
32
+ scope: string;
33
33
  /** Entity table ID for entity-scoped storage (NULL for app-level) */
34
34
  entityTableId: string | null;
35
35
  /** Qualified entity table name for ownerId lookups (NULL for app-level) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-presigned-url-plugin",
3
- "version": "0.19.2",
3
+ "version": "0.20.1",
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.1052.0",
44
44
  "@aws-sdk/s3-request-presigner": "^3.1052.0",
45
- "@pgpmjs/logger": "^2.11.1",
45
+ "@pgpmjs/logger": "^2.12.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.3"
57
57
  },
58
58
  "devDependencies": {
59
- "@constructive-io/s3-utils": "^2.17.2",
59
+ "@constructive-io/s3-utils": "^2.18.0",
60
60
  "@types/node": "^22.19.11",
61
61
  "makage": "^0.3.0"
62
62
  },
63
- "gitHead": "423992019655cc1e780149ce3f7e6e8c9b1c6e80"
63
+ "gitHead": "d398ac2288f57fa02713353ce36024a63dd6b9f1"
64
64
  }
package/plugin.js CHANGED
@@ -563,7 +563,7 @@ async function processSingleFile(options, txClient, storageConfig, databaseId, b
563
563
  // Auto-derive ltree path from custom key directory (only when has_path_shares)
564
564
  const derivedPath = isCustomKey && storageConfig.hasPathShares ? derivePathFromKey(s3Key) : null;
565
565
  // Create file record
566
- const hasOwnerColumn = storageConfig.membershipType !== null;
566
+ const hasOwnerColumn = storageConfig.scope !== 'app';
567
567
  const columns = ['bucket_id', 'key', 'content_hash', 'mime_type', 'size', 'filename', 'is_public'];
568
568
  const values = [bucket.id, s3Key, contentHash, contentType, size, filename || null, bucket.is_public];
569
569
  if (hasOwnerColumn) {
@@ -3,7 +3,7 @@ import type { StorageModuleConfig, BucketConfig } from './types';
3
3
  * Resolve the app-level storage module config for a database, using the LRU cache.
4
4
  *
5
5
  * This is the default path when no ownerId is provided. It returns the
6
- * storage module with membership_type IS NULL (app-level / database-wide).
6
+ * storage module with scope = 'app' (app-level / database-wide).
7
7
  *
8
8
  * @param pgClient - A pg client from the Graphile context (withPgClient or pgClient)
9
9
  * @param databaseId - The metaschema database UUID
@@ -43,14 +43,14 @@ const storageModuleCache = new lru_cache_1.LRUCache({
43
43
  * SQL query to resolve the app-level storage module config for a database.
44
44
  *
45
45
  * Joins storage_module β†’ table β†’ schema to get fully-qualified table names.
46
- * Filters to app-level (membership_type IS NULL) by default.
46
+ * Filters to app-level (scope = 'app') by default.
47
47
  *
48
- * Requires the multi-scope schema (membership_type column on storage_module).
48
+ * Requires the multi-scope schema (scope column on storage_module).
49
49
  */
50
50
  const APP_STORAGE_MODULE_QUERY = `
51
51
  SELECT
52
52
  sm.id,
53
- sm.membership_type,
53
+ sm.scope,
54
54
  sm.entity_table_id,
55
55
  bs.schema_name AS buckets_schema,
56
56
  bt.name AS buckets_table,
@@ -76,7 +76,7 @@ const APP_STORAGE_MODULE_QUERY = `
76
76
  JOIN metaschema_public.table ft ON ft.id = sm.files_table_id
77
77
  JOIN metaschema_public.schema fs ON fs.id = ft.schema_id
78
78
  WHERE sm.database_id = $1
79
- AND sm.membership_type IS NULL
79
+ AND sm.scope = 'app'
80
80
  LIMIT 1
81
81
  `;
82
82
  /**
@@ -88,7 +88,7 @@ const APP_STORAGE_MODULE_QUERY = `
88
88
  const ALL_STORAGE_MODULES_QUERY = `
89
89
  SELECT
90
90
  sm.id,
91
- sm.membership_type,
91
+ sm.scope,
92
92
  sm.entity_table_id,
93
93
  bs.schema_name AS buckets_schema,
94
94
  bt.name AS buckets_table,
@@ -129,7 +129,7 @@ function buildConfig(row) {
129
129
  schemaName: row.buckets_schema,
130
130
  bucketsTableName: row.buckets_table,
131
131
  filesTableName: row.files_table,
132
- membershipType: row.membership_type,
132
+ scope: row.scope,
133
133
  entityTableId: row.entity_table_id,
134
134
  entityQualifiedName: row.entity_schema && row.entity_table
135
135
  ? quotes_1.QuoteUtils.quoteQualifiedIdentifier(row.entity_schema, row.entity_table)
@@ -152,7 +152,7 @@ function buildConfig(row) {
152
152
  * Resolve the app-level storage module config for a database, using the LRU cache.
153
153
  *
154
154
  * This is the default path when no ownerId is provided. It returns the
155
- * storage module with membership_type IS NULL (app-level / database-wide).
155
+ * storage module with scope = 'app' (app-level / database-wide).
156
156
  *
157
157
  * @param pgClient - A pg client from the Graphile context (withPgClient or pgClient)
158
158
  * @param databaseId - The metaschema database UUID
@@ -213,11 +213,9 @@ async function getStorageModuleConfigForOwner(pgClient, databaseId, ownerId) {
213
213
  log.debug(`Loading all storage modules for database ${databaseId} to resolve ownerId ${ownerId}`);
214
214
  const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] });
215
215
  allConfigs = result.rows.map(buildConfig);
216
- // Cache each individual config by its membership type
216
+ // Cache each individual config by its scope
217
217
  for (const config of allConfigs) {
218
- const key = config.membershipType === null
219
- ? `storage:${databaseId}:app`
220
- : `storage:${databaseId}:mt:${config.membershipType}`;
218
+ const key = `storage:${databaseId}:scope:${config.scope}`;
221
219
  storageModuleCache.set(key, config);
222
220
  }
223
221
  }
@@ -232,7 +230,7 @@ async function getStorageModuleConfigForOwner(pgClient, databaseId, ownerId) {
232
230
  // Found the matching module β€” cache the ownerIdβ†’module mapping
233
231
  storageModuleCache.set(ownerCacheKey, mod);
234
232
  log.debug(`Resolved ownerId ${ownerId} to storage module ${mod.id} ` +
235
- `(membershipType=${mod.membershipType}, table=${mod.bucketsQualifiedName})`);
233
+ `(scope=${mod.scope}, table=${mod.bucketsQualifiedName})`);
236
234
  return mod;
237
235
  }
238
236
  }
@@ -284,16 +282,16 @@ async function loadAllStorageModules(pgClient, databaseId) {
284
282
  log.debug(`Loading all storage modules for database ${databaseId}`);
285
283
  const result = await pgClient.query({ text: ALL_STORAGE_MODULES_QUERY, values: [databaseId] });
286
284
  const configs = result.rows.map(buildConfig);
287
- // Cache each individual config by its membership type
285
+ // Cache each individual config by its scope
288
286
  for (const config of configs) {
289
- const key = config.membershipType === null
290
- ? `storage:${databaseId}:app`
291
- : `storage:${databaseId}:mt:${config.membershipType}`;
287
+ const key = `storage:${databaseId}:scope:${config.scope}`;
292
288
  storageModuleCache.set(key, config);
293
289
  }
294
- // Store the full list under a sentinel key
295
- const sentinel = { ...configs[0] || {}, _allConfigs: configs };
296
- storageModuleCache.set(cacheKey, sentinel);
290
+ // Store the full list under a sentinel key (only if non-empty to avoid caching failed lookups)
291
+ if (configs.length > 0) {
292
+ const sentinel = { ...configs[0], _allConfigs: configs };
293
+ storageModuleCache.set(cacheKey, sentinel);
294
+ }
297
295
  return configs;
298
296
  }
299
297
  /**
@@ -356,14 +354,15 @@ async function getBucketConfig(pgClient, storageConfig, databaseId, bucketKey, o
356
354
  log.debug(`Bucket cache miss for ${databaseId}:${bucketKey}${ownerId ? ` (owner=${ownerId})` : ''}, querying DB...`);
357
355
  // Entity-scoped buckets use (owner_id, key) composite lookup;
358
356
  // app-level buckets just use key.
359
- const hasOwner = ownerId && storageConfig.membershipType !== null;
357
+ const isEntityScoped = storageConfig.scope !== 'app';
358
+ const hasOwner = ownerId && isEntityScoped;
360
359
  const result = await pgClient.query({
361
360
  text: hasOwner
362
361
  ? `SELECT id, key, type, is_public, owner_id, allowed_mime_types, max_file_size, allow_custom_keys
363
362
  FROM ${storageConfig.bucketsQualifiedName}
364
363
  WHERE key = $1 AND owner_id = $2
365
364
  LIMIT 1`
366
- : `SELECT id, key, type, is_public, ${storageConfig.membershipType !== null ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys
365
+ : `SELECT id, key, type, is_public, ${isEntityScoped ? 'owner_id,' : ''} allowed_mime_types, max_file_size, allow_custom_keys
367
366
  FROM ${storageConfig.bucketsQualifiedName}
368
367
  WHERE key = $1
369
368
  LIMIT 1`,
@@ -384,7 +383,7 @@ async function getBucketConfig(pgClient, storageConfig, databaseId, bucketKey, o
384
383
  allow_custom_keys: row.allow_custom_keys ?? false,
385
384
  };
386
385
  bucketCache.set(cacheKey, config);
387
- log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.membershipType ?? 'app'})`);
386
+ log.debug(`Cached bucket config for ${databaseId}:${bucketKey} (id=${config.id}, scope=${storageConfig.scope})`);
388
387
  return config;
389
388
  }
390
389
  // --- S3 bucket existence cache ---
package/types.d.ts CHANGED
@@ -28,8 +28,8 @@ export interface StorageModuleConfig {
28
28
  bucketsTableName: string;
29
29
  /** Files table name */
30
30
  filesTableName: string;
31
- /** Membership type (NULL for app-level, non-NULL for entity-scoped) */
32
- membershipType: number | null;
31
+ /** Scope name (e.g., 'app', 'org', 'team') */
32
+ scope: string;
33
33
  /** Entity table ID for entity-scoped storage (NULL for app-level) */
34
34
  entityTableId: string | null;
35
35
  /** Qualified entity table name for ownerId lookups (NULL for app-level) */