graphile-bucket-provisioner-plugin 1.17.1 → 1.18.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.
Files changed (3) hide show
  1. package/esm/plugin.js +8 -91
  2. package/package.json +5 -5
  3. package/plugin.js +8 -91
package/esm/plugin.js CHANGED
@@ -21,24 +21,7 @@ import { extendSchema, gql } from 'graphile-utils';
21
21
  * resolution in the explicit provisionBucket mutation).
22
22
  */
23
23
  const ALL_STORAGE_MODULES_QUERY = `
24
- SELECT
25
- sm.id,
26
- sm.database_id,
27
- sm.buckets_table_id,
28
- sm.scope,
29
- sm.entity_field,
30
- sm.entity_table_id,
31
- bs.schema_name AS buckets_schema,
32
- bt.name AS buckets_table,
33
- sm.allowed_origins,
34
- es.schema_name AS entity_schema,
35
- et.name AS entity_table
36
- FROM metaschema_modules_public.storage_module sm
37
- JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id
38
- JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
39
- LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
40
- LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
41
- WHERE sm.database_id = $1
24
+ SELECT * FROM function_resolution.storage_modules()
42
25
  `;
43
26
  /**
44
27
  * Run a query against a grafast `withPgClient` client.
@@ -73,7 +56,7 @@ function scopeKeyColumn(storageModule) {
73
56
  * silently picking.
74
57
  */
75
58
  async function resolveBucketByKey(pgClient, databaseId, bucketKey, ownerId) {
76
- const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY, [databaseId]);
59
+ const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY);
77
60
  const modules = result.rows;
78
61
  if (modules.length === 0) {
79
62
  throw new Error(`STORAGE_MODULE_NOT_PROVISIONED: database ${databaseId} has no storage modules`);
@@ -124,81 +107,15 @@ async function resolveDatabaseId(pgClient) {
124
107
  const result = await runQuery(pgClient, `SELECT jwt_private.current_database_id() AS id`);
125
108
  return result.rows[0]?.id ?? null;
126
109
  }
127
- async function resolveEntityContext(pgClient, storageModule) {
128
- const result = await runQuery(pgClient, `SELECT r.entity_type, r.get_org_fn_schema, r.get_org_fn
129
- FROM metaschema.resolve_entity_context_by_field($1::uuid, $2::uuid, $3::text) r`, [storageModule.database_id, storageModule.buckets_table_id, storageModule.entity_field]);
130
- return result.rows[0] ?? {
131
- entity_type: null,
132
- get_org_fn_schema: null,
133
- get_org_fn: null,
134
- };
135
- }
136
110
  /**
137
- * Mirror the storage module generator's data_job_trigger shape from
138
- * `packages/metaschema-generators/deploy/schemas/metaschema_generators/procedures/storage_module.sql`
139
- * and the add_job argument assembly in
140
- * `packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/triggers/job_trigger.sql`.
141
- * A callable enqueue function beside the trigger would be the durable fix for
142
- * this deliberate mirroring, but is out of scope here.
143
- *
144
- * The three scope shapes differ only in the payload entries and the identity
145
- * arguments, so those are the only parts a caller supplies — the identifier,
146
- * queue, retry, and priority policy exist exactly once.
111
+ * Enqueue reconciliation through the storage module's generated
112
+ * `<private>.<buckets>_provision(bucket_id)`. It runs as the request role: it
113
+ * locks the bucket row FOR UPDATE under the buckets table's own RLS, then
114
+ * enqueues exactly the job the buckets INSERT trigger does.
147
115
  */
148
- function addJobStatement(payloadEntries, identityArgs) {
149
- return `SELECT (app_jobs.add_job(
150
- identifier => 'storage:provision_bucket',
151
- payload => json_build_object(${payloadEntries}),
152
- queue_name => 'bucket:' || $1::text,
153
- max_attempts => 25,
154
- priority => 0${identityArgs}
155
- )).id AS id`;
156
- }
157
116
  async function enqueueReconciliationJob(pgClient, storageModule, bucket) {
158
- const scope = storageModule.scope;
159
- const entityField = storageModule.entity_field;
160
- let text;
161
- let values;
162
- if (entityField === null) {
163
- text = addJobStatement(`'id', $1::uuid,
164
- 'scope', $2::text`, '');
165
- values = [bucket.id, scope];
166
- }
167
- else if (entityField === 'database_id') {
168
- const databaseId = bucket.scope_key;
169
- if (!databaseId) {
170
- throw new Error(`STORAGE_BUCKET_SCOPE_KEY_MISSING: bucket ${bucket.id} has no database_id`);
171
- }
172
- text = addJobStatement(`'database_id', $2::uuid,
173
- 'id', $1::uuid,
174
- 'scope', $3::text`, `,
175
- db_id => $2,
176
- entity_id => $2,
177
- organization_id => NULL,
178
- entity_type => $3`);
179
- values = [bucket.id, databaseId, scope];
180
- }
181
- else if (entityField === 'owner_id') {
182
- const ownerId = bucket.scope_key;
183
- if (!ownerId) {
184
- throw new Error(`STORAGE_BUCKET_SCOPE_KEY_MISSING: bucket ${bucket.id} has no owner_id`);
185
- }
186
- const context = await resolveEntityContext(pgClient, storageModule);
187
- const orgFunction = context.get_org_fn_schema && context.get_org_fn
188
- ? `${QuoteUtils.quoteQualifiedIdentifier(context.get_org_fn_schema, context.get_org_fn)}($3::text, $2::uuid)`
189
- : 'NULL';
190
- text = addJobStatement(`'id', $1::uuid,
191
- 'owner_id', $2::uuid,
192
- 'scope', $3::text`, `,
193
- entity_id => $2,
194
- organization_id => ${orgFunction},
195
- entity_type => $3`);
196
- values = [bucket.id, ownerId, scope];
197
- }
198
- else {
199
- throw new Error(`STORAGE_BUCKET_ENTITY_FIELD_UNSUPPORTED: ${entityField}`);
200
- }
201
- const result = await runQuery(pgClient, text, values);
117
+ const provisionFunction = QuoteUtils.quoteQualifiedIdentifier(storageModule.private_schema, `${storageModule.buckets_table}_provision`);
118
+ const result = await runQuery(pgClient, `SELECT ${provisionFunction}($1::uuid) AS id`, [bucket.id]);
202
119
  const jobId = result.rows[0]?.id;
203
120
  if (!jobId) {
204
121
  throw new Error(`STORAGE_BUCKET_JOB_ID_MISSING: bucket ${bucket.id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-bucket-provisioner-plugin",
3
- "version": "1.17.1",
3
+ "version": "1.18.1",
4
4
  "description": "Bucket provisioning plugin for PostGraphile v5 — explicitly provisions S3 buckets via a GraphQL mutation",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -40,10 +40,10 @@
40
40
  "url": "https://github.com/constructive-io/constructive/issues"
41
41
  },
42
42
  "dependencies": {
43
- "@constructive-io/bucket-provisioner": "^0.30.1",
44
- "@pgpmjs/logger": "^2.26.0",
43
+ "@constructive-io/bucket-provisioner": "^0.31.0",
44
+ "@pgpmjs/logger": "^2.27.0",
45
45
  "@pgsql/quotes": "^18.2.4",
46
- "graphile-storage-registry": "^0.2.0"
46
+ "graphile-storage-registry": "^0.3.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "grafast": "^1.1.1",
@@ -58,5 +58,5 @@
58
58
  "@types/node": "^22.19.11",
59
59
  "makage": "^0.8.0"
60
60
  },
61
- "gitHead": "8227c782c98281f7185d257a7f716fbffdcc45c2"
61
+ "gitHead": "0b4ffa11a6d7958ba1068ce8e19616d4b018c0d6"
62
62
  }
package/plugin.js CHANGED
@@ -25,24 +25,7 @@ const graphile_utils_1 = require("graphile-utils");
25
25
  * resolution in the explicit provisionBucket mutation).
26
26
  */
27
27
  const ALL_STORAGE_MODULES_QUERY = `
28
- SELECT
29
- sm.id,
30
- sm.database_id,
31
- sm.buckets_table_id,
32
- sm.scope,
33
- sm.entity_field,
34
- sm.entity_table_id,
35
- bs.schema_name AS buckets_schema,
36
- bt.name AS buckets_table,
37
- sm.allowed_origins,
38
- es.schema_name AS entity_schema,
39
- et.name AS entity_table
40
- FROM metaschema_modules_public.storage_module sm
41
- JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id
42
- JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
43
- LEFT JOIN metaschema_public.table et ON et.id = sm.entity_table_id
44
- LEFT JOIN metaschema_public.schema es ON es.id = et.schema_id
45
- WHERE sm.database_id = $1
28
+ SELECT * FROM function_resolution.storage_modules()
46
29
  `;
47
30
  /**
48
31
  * Run a query against a grafast `withPgClient` client.
@@ -77,7 +60,7 @@ function scopeKeyColumn(storageModule) {
77
60
  * silently picking.
78
61
  */
79
62
  async function resolveBucketByKey(pgClient, databaseId, bucketKey, ownerId) {
80
- const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY, [databaseId]);
63
+ const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY);
81
64
  const modules = result.rows;
82
65
  if (modules.length === 0) {
83
66
  throw new Error(`STORAGE_MODULE_NOT_PROVISIONED: database ${databaseId} has no storage modules`);
@@ -128,81 +111,15 @@ async function resolveDatabaseId(pgClient) {
128
111
  const result = await runQuery(pgClient, `SELECT jwt_private.current_database_id() AS id`);
129
112
  return result.rows[0]?.id ?? null;
130
113
  }
131
- async function resolveEntityContext(pgClient, storageModule) {
132
- const result = await runQuery(pgClient, `SELECT r.entity_type, r.get_org_fn_schema, r.get_org_fn
133
- FROM metaschema.resolve_entity_context_by_field($1::uuid, $2::uuid, $3::text) r`, [storageModule.database_id, storageModule.buckets_table_id, storageModule.entity_field]);
134
- return result.rows[0] ?? {
135
- entity_type: null,
136
- get_org_fn_schema: null,
137
- get_org_fn: null,
138
- };
139
- }
140
114
  /**
141
- * Mirror the storage module generator's data_job_trigger shape from
142
- * `packages/metaschema-generators/deploy/schemas/metaschema_generators/procedures/storage_module.sql`
143
- * and the add_job argument assembly in
144
- * `packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/triggers/job_trigger.sql`.
145
- * A callable enqueue function beside the trigger would be the durable fix for
146
- * this deliberate mirroring, but is out of scope here.
147
- *
148
- * The three scope shapes differ only in the payload entries and the identity
149
- * arguments, so those are the only parts a caller supplies — the identifier,
150
- * queue, retry, and priority policy exist exactly once.
115
+ * Enqueue reconciliation through the storage module's generated
116
+ * `<private>.<buckets>_provision(bucket_id)`. It runs as the request role: it
117
+ * locks the bucket row FOR UPDATE under the buckets table's own RLS, then
118
+ * enqueues exactly the job the buckets INSERT trigger does.
151
119
  */
152
- function addJobStatement(payloadEntries, identityArgs) {
153
- return `SELECT (app_jobs.add_job(
154
- identifier => 'storage:provision_bucket',
155
- payload => json_build_object(${payloadEntries}),
156
- queue_name => 'bucket:' || $1::text,
157
- max_attempts => 25,
158
- priority => 0${identityArgs}
159
- )).id AS id`;
160
- }
161
120
  async function enqueueReconciliationJob(pgClient, storageModule, bucket) {
162
- const scope = storageModule.scope;
163
- const entityField = storageModule.entity_field;
164
- let text;
165
- let values;
166
- if (entityField === null) {
167
- text = addJobStatement(`'id', $1::uuid,
168
- 'scope', $2::text`, '');
169
- values = [bucket.id, scope];
170
- }
171
- else if (entityField === 'database_id') {
172
- const databaseId = bucket.scope_key;
173
- if (!databaseId) {
174
- throw new Error(`STORAGE_BUCKET_SCOPE_KEY_MISSING: bucket ${bucket.id} has no database_id`);
175
- }
176
- text = addJobStatement(`'database_id', $2::uuid,
177
- 'id', $1::uuid,
178
- 'scope', $3::text`, `,
179
- db_id => $2,
180
- entity_id => $2,
181
- organization_id => NULL,
182
- entity_type => $3`);
183
- values = [bucket.id, databaseId, scope];
184
- }
185
- else if (entityField === 'owner_id') {
186
- const ownerId = bucket.scope_key;
187
- if (!ownerId) {
188
- throw new Error(`STORAGE_BUCKET_SCOPE_KEY_MISSING: bucket ${bucket.id} has no owner_id`);
189
- }
190
- const context = await resolveEntityContext(pgClient, storageModule);
191
- const orgFunction = context.get_org_fn_schema && context.get_org_fn
192
- ? `${quotes_1.QuoteUtils.quoteQualifiedIdentifier(context.get_org_fn_schema, context.get_org_fn)}($3::text, $2::uuid)`
193
- : 'NULL';
194
- text = addJobStatement(`'id', $1::uuid,
195
- 'owner_id', $2::uuid,
196
- 'scope', $3::text`, `,
197
- entity_id => $2,
198
- organization_id => ${orgFunction},
199
- entity_type => $3`);
200
- values = [bucket.id, ownerId, scope];
201
- }
202
- else {
203
- throw new Error(`STORAGE_BUCKET_ENTITY_FIELD_UNSUPPORTED: ${entityField}`);
204
- }
205
- const result = await runQuery(pgClient, text, values);
121
+ const provisionFunction = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.private_schema, `${storageModule.buckets_table}_provision`);
122
+ const result = await runQuery(pgClient, `SELECT ${provisionFunction}($1::uuid) AS id`, [bucket.id]);
206
123
  const jobId = result.rows[0]?.id;
207
124
  if (!jobId) {
208
125
  throw new Error(`STORAGE_BUCKET_JOB_ID_MISSING: bucket ${bucket.id}`);