graphile-presigned-url-plugin 1.15.0 → 1.15.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.
@@ -68,11 +68,19 @@ export interface ManagedUploadTarget {
68
68
  * * a registered column names its storage module, and either a logical bucket
69
69
  * key or the reserved default tag for its declared publicness;
70
70
  * * an unregistered column (a bare `image`/`upload` on a database provisioned
71
- * before the registry) falls back to the app-scope module and the same
72
- * reserved default tag. That is a *tenant* default, not an environment one.
71
+ * before the registry) falls back to the database's single global storage
72
+ * plane and the same reserved default tag. That is a *tenant* default, not
73
+ * an environment one.
73
74
  *
74
- * A database with no storage module raises: there is nowhere tenant-owned to put
75
- * the bytes, and the deployment's configured bucket is not an answer.
75
+ * The fallback is by shape, not by scope name: a global plane is one with no
76
+ * entity key (`entity_table_id IS NULL`), which is what a database-wide plane is
77
+ * whether it registered as 'database', 'platform', or 'app'. Matching the name
78
+ * instead sent every tenant whose plane registers as 'database' — i.e. every
79
+ * database-scope plane — to STORAGE_MODULE_NOT_FOUND.
80
+ *
81
+ * A database with no global plane raises, and so does one with several: there is
82
+ * nowhere unambiguously tenant-owned to put the bytes, and the deployment's
83
+ * configured bucket is not an answer.
76
84
  */
77
85
  export declare function resolveManagedUploadTarget(args: {
78
86
  options: PresignedUrlPluginOptions;
@@ -55,11 +55,19 @@ export function buildFileProjection(file, bucket, s3) {
55
55
  * * a registered column names its storage module, and either a logical bucket
56
56
  * key or the reserved default tag for its declared publicness;
57
57
  * * an unregistered column (a bare `image`/`upload` on a database provisioned
58
- * before the registry) falls back to the app-scope module and the same
59
- * reserved default tag. That is a *tenant* default, not an environment one.
58
+ * before the registry) falls back to the database's single global storage
59
+ * plane and the same reserved default tag. That is a *tenant* default, not
60
+ * an environment one.
60
61
  *
61
- * A database with no storage module raises: there is nowhere tenant-owned to put
62
- * the bytes, and the deployment's configured bucket is not an answer.
62
+ * The fallback is by shape, not by scope name: a global plane is one with no
63
+ * entity key (`entity_table_id IS NULL`), which is what a database-wide plane is
64
+ * whether it registered as 'database', 'platform', or 'app'. Matching the name
65
+ * instead sent every tenant whose plane registers as 'database' — i.e. every
66
+ * database-scope plane — to STORAGE_MODULE_NOT_FOUND.
67
+ *
68
+ * A database with no global plane raises, and so does one with several: there is
69
+ * nowhere unambiguously tenant-owned to put the bytes, and the deployment's
70
+ * configured bucket is not an answer.
63
71
  */
64
72
  export async function resolveManagedUploadTarget(args) {
65
73
  const { options, withPgClient, pgSettings, databaseId, field, defaultPublicAccess } = args;
@@ -71,7 +79,7 @@ export async function resolveManagedUploadTarget(args) {
71
79
  }
72
80
  catch (err) {
73
81
  // An unregistered column is a legitimate state (it predates the registry)
74
- // and falls back to the tenant's app-scope default below. Any other
82
+ // and falls back to the tenant's global plane below. Any other
75
83
  // failure — a broken connection, a missing registry table — is not.
76
84
  if (err?.name === 'FileRefFieldNotRegisteredError')
77
85
  return null;
@@ -79,15 +87,24 @@ export async function resolveManagedUploadTarget(args) {
79
87
  }
80
88
  });
81
89
  const allConfigs = await withPgClient(null, (pgClient) => loadAllStorageModules(pgClient, databaseId));
90
+ const globalConfigs = allConfigs.filter((c) => c.entityTableId === null);
91
+ if (!binding && globalConfigs.length > 1) {
92
+ // Several global planes and no registry row to disambiguate: picking one is
93
+ // picking a tenant's bucket at random. Name them and refuse.
94
+ throw new Error(`STORAGE_MODULE_AMBIGUOUS: ${field.schemaName}.${field.tableName}.${field.columnName} is an ` +
95
+ `unregistered upload column and database ${databaseId} has ${globalConfigs.length} global ` +
96
+ `storage planes (scopes: ${globalConfigs.map((c) => c.scope).join(', ')}); register the column ` +
97
+ 'so it names the plane it writes to');
98
+ }
82
99
  const storageConfig = binding
83
100
  ? allConfigs.find((c) => c.id === binding.storageModuleId)
84
- : allConfigs.find((c) => c.scope === 'app');
101
+ : globalConfigs[0];
85
102
  if (!storageConfig) {
86
103
  throw new Error(binding
87
104
  ? `STORAGE_MODULE_NOT_FOUND: file_ref_field ${binding.id} names storage module ` +
88
105
  `${binding.storageModuleId}, which database ${databaseId} does not have`
89
106
  : `STORAGE_MODULE_NOT_FOUND: ${field.schemaName}.${field.tableName}.${field.columnName} is an ` +
90
- `unregistered upload column and database ${databaseId} has no app-scope storage module to ` +
107
+ `unregistered upload column and database ${databaseId} has no global storage plane to ` +
91
108
  'default to; there is no environment bucket to fall back to');
92
109
  }
93
110
  if (storageConfig.entityTableId !== null) {
@@ -68,11 +68,19 @@ export interface ManagedUploadTarget {
68
68
  * * a registered column names its storage module, and either a logical bucket
69
69
  * key or the reserved default tag for its declared publicness;
70
70
  * * an unregistered column (a bare `image`/`upload` on a database provisioned
71
- * before the registry) falls back to the app-scope module and the same
72
- * reserved default tag. That is a *tenant* default, not an environment one.
71
+ * before the registry) falls back to the database's single global storage
72
+ * plane and the same reserved default tag. That is a *tenant* default, not
73
+ * an environment one.
73
74
  *
74
- * A database with no storage module raises: there is nowhere tenant-owned to put
75
- * the bytes, and the deployment's configured bucket is not an answer.
75
+ * The fallback is by shape, not by scope name: a global plane is one with no
76
+ * entity key (`entity_table_id IS NULL`), which is what a database-wide plane is
77
+ * whether it registered as 'database', 'platform', or 'app'. Matching the name
78
+ * instead sent every tenant whose plane registers as 'database' — i.e. every
79
+ * database-scope plane — to STORAGE_MODULE_NOT_FOUND.
80
+ *
81
+ * A database with no global plane raises, and so does one with several: there is
82
+ * nowhere unambiguously tenant-owned to put the bytes, and the deployment's
83
+ * configured bucket is not an answer.
76
84
  */
77
85
  export declare function resolveManagedUploadTarget(args: {
78
86
  options: PresignedUrlPluginOptions;
package/managed-upload.js CHANGED
@@ -61,11 +61,19 @@ function buildFileProjection(file, bucket, s3) {
61
61
  * * a registered column names its storage module, and either a logical bucket
62
62
  * key or the reserved default tag for its declared publicness;
63
63
  * * an unregistered column (a bare `image`/`upload` on a database provisioned
64
- * before the registry) falls back to the app-scope module and the same
65
- * reserved default tag. That is a *tenant* default, not an environment one.
64
+ * before the registry) falls back to the database's single global storage
65
+ * plane and the same reserved default tag. That is a *tenant* default, not
66
+ * an environment one.
66
67
  *
67
- * A database with no storage module raises: there is nowhere tenant-owned to put
68
- * the bytes, and the deployment's configured bucket is not an answer.
68
+ * The fallback is by shape, not by scope name: a global plane is one with no
69
+ * entity key (`entity_table_id IS NULL`), which is what a database-wide plane is
70
+ * whether it registered as 'database', 'platform', or 'app'. Matching the name
71
+ * instead sent every tenant whose plane registers as 'database' — i.e. every
72
+ * database-scope plane — to STORAGE_MODULE_NOT_FOUND.
73
+ *
74
+ * A database with no global plane raises, and so does one with several: there is
75
+ * nowhere unambiguously tenant-owned to put the bytes, and the deployment's
76
+ * configured bucket is not an answer.
69
77
  */
70
78
  async function resolveManagedUploadTarget(args) {
71
79
  const { options, withPgClient, pgSettings, databaseId, field, defaultPublicAccess } = args;
@@ -77,7 +85,7 @@ async function resolveManagedUploadTarget(args) {
77
85
  }
78
86
  catch (err) {
79
87
  // An unregistered column is a legitimate state (it predates the registry)
80
- // and falls back to the tenant's app-scope default below. Any other
88
+ // and falls back to the tenant's global plane below. Any other
81
89
  // failure — a broken connection, a missing registry table — is not.
82
90
  if (err?.name === 'FileRefFieldNotRegisteredError')
83
91
  return null;
@@ -85,15 +93,24 @@ async function resolveManagedUploadTarget(args) {
85
93
  }
86
94
  });
87
95
  const allConfigs = await withPgClient(null, (pgClient) => (0, storage_module_cache_1.loadAllStorageModules)(pgClient, databaseId));
96
+ const globalConfigs = allConfigs.filter((c) => c.entityTableId === null);
97
+ if (!binding && globalConfigs.length > 1) {
98
+ // Several global planes and no registry row to disambiguate: picking one is
99
+ // picking a tenant's bucket at random. Name them and refuse.
100
+ throw new Error(`STORAGE_MODULE_AMBIGUOUS: ${field.schemaName}.${field.tableName}.${field.columnName} is an ` +
101
+ `unregistered upload column and database ${databaseId} has ${globalConfigs.length} global ` +
102
+ `storage planes (scopes: ${globalConfigs.map((c) => c.scope).join(', ')}); register the column ` +
103
+ 'so it names the plane it writes to');
104
+ }
88
105
  const storageConfig = binding
89
106
  ? allConfigs.find((c) => c.id === binding.storageModuleId)
90
- : allConfigs.find((c) => c.scope === 'app');
107
+ : globalConfigs[0];
91
108
  if (!storageConfig) {
92
109
  throw new Error(binding
93
110
  ? `STORAGE_MODULE_NOT_FOUND: file_ref_field ${binding.id} names storage module ` +
94
111
  `${binding.storageModuleId}, which database ${databaseId} does not have`
95
112
  : `STORAGE_MODULE_NOT_FOUND: ${field.schemaName}.${field.tableName}.${field.columnName} is an ` +
96
- `unregistered upload column and database ${databaseId} has no app-scope storage module to ` +
113
+ `unregistered upload column and database ${databaseId} has no global storage plane to ` +
97
114
  'default to; there is no environment bucket to fall back to');
98
115
  }
99
116
  if (storageConfig.entityTableId !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-presigned-url-plugin",
3
- "version": "1.15.0",
3
+ "version": "1.15.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",
@@ -62,5 +62,5 @@
62
62
  "@types/node": "^22.19.11",
63
63
  "makage": "^0.3.0"
64
64
  },
65
- "gitHead": "d10fe91ede80a4c2dfe2c1c6dcf6fe7193784eba"
65
+ "gitHead": "e35db12526094b515596ced2dd27a1ec8edd99d5"
66
66
  }